#!/usr/bin/env python3 import sys import yaml import re import os from typing import Dict, List, Optional, Tuple FileName = str decl_dict = { "BOOL": "bool @NAME@", "CHAR": "char @NAME@", "INT8": "int8_t @NAME@", "INT16": "int16_t @NAME@", "INT32": "int32_t @NAME@", "INT64": "int64_t @NAME@", "INTPTR": "intptr_t @NAME@", "UINT8": "uint8_t @NAME@", "UINT16": "uint16_t @NAME@", "UINT32": "uint32_t @NAME@", "UINT64": "uint64_t @NAME@", "UINTPTR": "uintptr_t @NAME@", "FLOAT": "float @NAME@", "DOUBLE": "double @NAME@", "CHAR_ARRAY": "char @NAME@[]", } test_assign_dict = { "CHAR_ARRAY": """\ { /* TOP_C: #include <string.h> */ const char @NAME@_orig[] = @VALUE@; strncpy(@NAME@, @NAME@_orig, sizeof(@NAME@)); } """, } testing_reset_toggles_decl = f"""\ /** @brief Reset toggles to the initialization values. * * Call this function in the setup of a test suite to start every * test with consistent values. * * This function is available only when testing is enabled (when * `TESTING` is defined as 1). */ void testing_reset_toggles(void); """ def printerr(*args, **kwargs): print(*args, **kwargs, file=sys.stderr) def is_empty(s): return s is None def read_yaml_file(yaml_input: FileName) -> Tuple[List[str], List[str]]: """Read from YAML file, return tuple with header and data.""" with open(yaml_input) as yaml_file: data = yaml.load( yaml_file, Loader=yaml.BaseLoader, # SafeLoader ) if data is None: return [] return list(data) def read_defaults(yaml_input: FileName) -> Dict[str, Dict[str, str]]: """Read default values from YAML file, return dict with name and data.""" # Read from YAML file data = read_yaml_file(yaml_input) necessary = {"NAME", "DEFAULT", "TYPE", "DECL", "BRIEF"} optional = {"DESCRIPTION", "H", "C", "TEST_ASSIGN"} all_known = {*necessary, *optional} # Insert data in the dict defaults = {} for x in data: # Assert necessary fields are present for field in necessary: assert field in x, f"Field '{field}' is missing on data {x}" # Add missing optional fields for field in optional: if field not in x: x[field] = None # Warn any unknown fields for y in x: if y not in all_known: printerr(f"Warning: unknown field '{y}' in data {x}") # Assert option is not repeated name = x["NAME"] assert name not in defaults, f"Option '{name}' is duplicated on {x}" defaults[name] = x return defaults def read_char_ids( yaml_input: FileName, defaults: Dict[str, Dict[str, str]] = {}, ) -> Dict[str, Dict[str, str]]: """ Read characterizations from YAML file, return dict with char_id and data. """ # Read from YAML file data = read_yaml_file(yaml_input) necessary = {"CHAR_ID", "BRIEF"} optional = {"DESCRIPTION", "BASED_ON"} all_known = {*necessary, *optional, *defaults.keys()} # Insert data in the dict char_ids = {} for x in data: # Assert necessary fields are present for field in necessary: assert field in x, f"Field '{field}' is missing on data {x}" # Add missing optional fields for field in optional: if field not in x: x[field] = None # Warn any unknown fields for y in x: if y not in all_known: printerr(f"Warning: unknown field '{y}' in data {x}") # Assert char ID is not repeated name = x["CHAR_ID"] assert name not in char_ids, f"Char ID '{name}' is duplicated on {x}" # Process char ID based on other char ID based_on = x["BASED_ON"] if based_on is None: xx = x else: if isinstance(based_on, str): based_on = [based_on] elif isinstance(based_on, list): pass else: assert False, f"BASED_ON must be a string or list of strings" xx = {} for base in based_on: assert isinstance( base, str ), f"BASED_ON must be a string or list of strings" assert base in char_ids, ( f"Char ID '{base}' is not defined." f" Available: {list(char_ids.keys())}." ) xx.update(char_ids[base]) xx.update(x) char_ids[name] = xx return char_ids def write_characterization_header( defaults: Dict[str, Dict[str, str]], char_ids: Dict[str, Dict[str, str]], code_output: FileName = "include/toggle.h", ): #################################################################### # Code begin code_begin = """\ #ifndef TOGGLE_H #define TOGGLE_H #ifdef __cplusplus extern "C" { #endif /** @file toggle.h * @brief Toggle definitions. * * This file validates the macro CHAR_ID and includes the corresponding * Toggle header. * * In the list of CHAR_IDS, always add to the end of the list and * increment NUM_CHAR_IDS. * * Numbering starts with 1 because the compiler would treat an undefined * CHAR_ID as 0. */ #include <stdbool.h> #include <stdint.h> #ifdef DOXYGEN /** @brief Characterization ID (int). * * Define the device characterization: the options and features enabled. * * The characterization ID should be defined when calling CMake (ex: * `cmake -D CHAR_ID=...`) or when calling the compiler (ex: * `gcc -D CHAR_ID=...`). */ #define CHAR_ID CHAR_ID_TEST #endif #ifndef CHAR_ID #define CHAR_ID CHAR_ID_TEST #warning "CHAR_ID is not defined. Using default." #endif """ #################################################################### # Code end code_end = """\ #ifdef __cplusplus } #endif #endif /* TOGGLE_H */ """ #################################################################### # Options documentation and default values code_option_doc = """\ /* Options documentation. */ #ifdef DOXYGEN """ for name, data in defaults.items(): code_option_doc += f"""\ {apply_indent(format_brief_descr_comment(data['BRIEF'], data['DESCRIPTION']), indent=4)} {apply_indent(format_h_declaration(data), indent=4)} """ code_option_doc += apply_indent(testing_reset_toggles_decl, indent=4) code_option_doc += """ #endif /* DOXYGEN */ """ #################################################################### # Characterization IDs code_char_ids = """\ /* List of CHAR_IDs. */ """ num = -1 # Set variable for empty char_ids for num, items in enumerate(char_ids.items()): char_id, data = items code_char_ids += ( f"#define {char_id} {num+1} /**< @brief {data['BRIEF']} */\n" ) code_char_ids += f""" #define NUM_CHAR_IDS {num+1} /**< @brief Number of char IDs. */ /* Validate CHAR_ID range. */ #if (CHAR_ID < 1 || CHAR_ID > NUM_CHAR_IDS) #error "Macro CHAR_ID is not in the valid range." #endif """ #################################################################### # Characterization inclusions code_char_includes = """\ /* Include the characterization. */ #ifdef DOXYGEN /* Nothing to include for Doxygen. */ """ for char_id in char_ids: code_char_includes += ( f"#elif (CHAR_ID == {char_id})\n" f' #include "characterizations/{char_id.lower()}.h"\n' ) code_char_includes += f"#endif\n\n" #################################################################### # Fit everything together and write code = ( code_begin + code_option_doc + code_char_ids + code_char_includes + code_end ) code = clean_code(code) create_directory(code_output) with open(code_output, "w") as fp: fp.write(code) def create_directory(filename: FileName): directory = re.sub(r"/[^/]*$", r"", filename) os.makedirs(directory, exist_ok=True) def format_brief_descr_comment( brief: str, descr: str, mid_comment: bool = False ) -> str: comment = "" if mid_comment else "/** " if is_empty(descr): comment += f"@brief {brief}" comment += "" if mid_comment else " */" else: comment += f"@brief {brief}" + format_comment("\n" + descr) comment += "" if mid_comment else f"\n */" return comment def apply_indent(code: str, indent: int) -> str: indent = " " * indent return indent + ("\n" + indent).join(code.split("\n")) def write_characterization_source( defaults: Dict[str, Dict[str, str]], char_ids: Dict[str, Dict[str, str]], code_output: FileName = "src/toggle.c", ): #################################################################### # Code begin code_begin = """\ #include "toggle.h" """ #################################################################### # Code end code_end = "" #################################################################### # Characterization inclusions code_char_includes = """\ #ifdef DOXYGEN /* Nothing to include for Doxygen. */ """ for char_id in char_ids: code_char_includes += ( f"#elif (CHAR_ID == {char_id})\n" f' #include "characterizations/{char_id.lower()}.c"\n' ) code_char_includes += f"#endif\n" #################################################################### # Fit everything together and write code = code_begin + code_char_includes + code_end code = clean_code(code) create_directory(code_output) with open(code_output, "w") as fp: fp.write(code) def write_char_id_source( defaults: Dict[str, Dict[str, str]], char_id: Dict[str, Dict[str, str]], ): file_name = char_id["CHAR_ID"].lower() + ".c" code_output = f"src/characterizations/{file_name}" #################################################################### # Code begin code_begin = "" #################################################################### # Code end code_end = "" #################################################################### # Options value code_option = "" for name, data in defaults.items(): code_option += f"""\ {format_c_definition(data, char_id)} """ #################################################################### # Testing functions if is_testing(char_id): code_option += """ void testing_reset_toggles(void) { """ for name, data in defaults.items(): code_option += f"""\ {apply_indent(format_c_assignment(data, char_id), indent=4)} """ code_option += "}\n" #################################################################### # Necessary headers necessary_headers = "" found_headers = re.findall( r"TOP_C: (.*?)(?:\s*\*\/)?$", code_option, re.MULTILINE ) found_headers = list(set(found_headers)) necessary_headers += "\n".join(found_headers) necessary_headers += "\n" #################################################################### # Fit everything together and write code = code_begin + necessary_headers + code_option + code_end code = clean_code(code) create_directory(code_output) with open(code_output, "w") as fp: fp.write(code) def write_char_id_header( defaults: Dict[str, Dict[str, str]], char_id: Dict[str, Dict[str, str]], ): file_name = char_id["CHAR_ID"].lower() + ".h" header_guard = "CHARACTERIZATIONS_" + char_id["CHAR_ID"].upper() + "_H" code_output = f"include/characterizations/{file_name}" #################################################################### # Code begin code_begin = f"""\ #ifndef {header_guard} #define {header_guard} /** @file {file_name} * {format_brief_descr_comment( char_id['BRIEF'], char_id["DESCRIPTION"], mid_comment=True )} */ """ #################################################################### # Code end code_end = f""" #endif /* {header_guard} */ """ #################################################################### # Options value code_option = "" for name, data in defaults.items(): code_option += f"""\ {format_brief_descr_comment(data['BRIEF'], data['DESCRIPTION'])} {format_h_declaration(data, char_id)} """ #################################################################### # Testing functions if is_testing(char_id): code_option += apply_indent(testing_reset_toggles_decl, indent=0) #################################################################### # Fit everything together and write code = code_begin + code_option + code_end code = clean_code(code) create_directory(code_output) with open(code_output, "w") as fp: fp.write(code) def format_comment(comment: str, *, indent: int = 0) -> str: comment = comment.rstrip() lines = ["", *comment.split("\n")] comment = f"\n{' ' * indent} * ".join(lines) return comment def format_h_declaration( defaults: Dict[str, str], char_id: Optional[Dict[str, str]] = None ) -> str: code = defaults["H"] return format_ch_def_decl(code, defaults, char_id, format="decl") def format_c_definition( defaults: Dict[str, str], char_id: Optional[Dict[str, str]] = None ) -> str: code = defaults["C"] return format_ch_def_decl(code, defaults, char_id, format="def") def format_c_assignment( defaults: Dict[str, str], char_id: Optional[Dict[str, str]] = None ) -> str: code = defaults["TEST_ASSIGN"] return format_ch_def_decl(code, defaults, char_id, format="assign") def is_testing(char_id: Optional[Dict[str, str]] = None) -> bool: if char_id is None or "TESTING" not in char_id or char_id["TESTING"] == "": testing = False else: testing = bool(int(char_id["TESTING"])) return testing def get_value( name: str, defaults: Dict[str, str], char_id: Optional[Dict[str, str]] = None, ) -> str: # Get value from characterization or defaults if char_id is None or name not in char_id or char_id[name] == "": value = defaults["DEFAULT"] else: value = char_id[name] return value def error_if_value_option_is_set_on_characterization_file( name: str, defaults: Dict[str, str], char_id: Dict[str, str], ) -> str: # Error if a "VALUE option" is being set on the characterization. if char_id is None: pass elif defaults["TYPE"].startswith("VALUE") and name in char_id: assert name not in char_id, ( f"{name} is declared with TYPE = VALUE and cannot be " "redefined in the characterization. " f"Remove {name} from the file yaml/char_ids.yaml." ) def format_ch_def_decl( code: str, defaults: Dict[str, str], char_id: Dict[str, str], *, format: str, ): typ = defaults["TYPE"] decl = defaults["DECL"] name = defaults["NAME"] value = get_value(name, defaults, char_id) testing = is_testing(char_id) testing_changes = False error_if_value_option_is_set_on_characterization_file( name, defaults, char_id ) # No custom code. Generate default. # TYPE and DECL if typ == "VALUE": testing_changes = False elif typ == "OPTION": testing_changes = True else: raise NotImplementedError( f'TYPE = "{typ}" is not implemented. ' 'Valid: ["VALUE", "OPTION"].' ) # DECL part 1 if decl == "MACRO": # MACRO (without type) cannot be tested testing_changes = False elif decl == "CUSTOM": # CUSTOM cannot be tested testing_changes = False if is_empty(code): # Make sure it is at lease an empty string code = "" # DECL part 2 if decl == "MACRO": # MACRO (without type) cannot be tested base = "MACRO" decl = "MACRO" elif decl == "CUSTOM": # CUSTOM cannot be tested # Nothing to do for custom return code elif ( decl.startswith("MACRO_") or decl.startswith("CONST_") or decl.startswith("VAR_") ): # MACRO_*, CONST_* or VAR_* base, orig_decl = decl.split("_", 1) if orig_decl in decl_dict: decl = decl_dict[orig_decl] else: raise NotImplementedError( f'DECL = "{decl}" is not implemented. ' 'Valid: ["*_UINT8", etc].' ) if orig_decl in test_assign_dict: test_assign = test_assign_dict[orig_decl] else: test_assign = "@NAME@ = @VALUE@;" else: raise NotImplementedError( f'DECL = "{decl}" is not implemented. ' 'Valid: ["MACRO_*", "CONST_*", "VAR_*"].' ) # Changes when testing if base == "MACRO": if testing and testing_changes: # When testing and not a value, a macro is transformed # in a non-const variable if format == "decl": code = f"extern {decl};" elif format == "def": code = f"{decl} = @VALUE@;" elif format == "assign": code = test_assign else: if format == "decl": code = f"#define @NAME@ @VALUE@" elif format == "def": code = "" elif format == "assign": code = "" elif base == "CONST": if format == "decl": code = f"extern @CONST@ {decl};" elif format == "def": code = f"@CONST@ {decl} = @VALUE@;" elif testing and testing_changes: # When testing and not a value, a const is transformed # in a non-const variable if format == "assign": code = test_assign elif base == "VAR": if format == "decl": code = f"extern {decl};" elif format == "def": code = f"{decl} = @VALUE@;" elif format == "assign": code = test_assign # Update values on the code code = code.replace("@NAME@", name) code = code.replace("@VALUE@", value) const = "" if testing and testing_changes else "const" if const == "": code = code.replace("@CONST@ ", const) code = code.replace(" @CONST@", const) else: code = code.replace("@CONST@", const) return code def clean_code(code: str) -> str: # One LF at the end of the file code = code.strip() + "\n" # Remove trailing whitespace code = re.sub(r"[ \t]+$", r"", code, flags=re.MULTILINE) # One new line before and after braces code = re.sub(r"\n\n+}", r"\n}", code) code = re.sub(r"{\n\n+", r"{\n", code) # No multiple new lines code = re.sub(r"\n\n\n+", r"\n\n", code) return code def main(): defaults = read_defaults("yaml/defaults.yaml") char_ids = read_char_ids("yaml/char_ids.yaml", defaults=defaults) write_characterization_header(defaults, char_ids) write_characterization_source(defaults, char_ids) for name, char_id in char_ids.items(): write_char_id_header(defaults, char_id) write_char_id_source(defaults, char_id) if __name__ == "__main__": main()