Source code for peakrdl_pybind11.exporter

"""
Main exporter implementation for PeakRDL-pybind11
"""

import keyword
import os
import re
from collections import OrderedDict
from pathlib import Path
from typing import TypedDict

from jinja2 import Environment, PackageLoader, select_autoescape
from systemrdl.node import (
    AddrmapNode,
    FieldNode,
    MemNode,
    Node,
    RegfileNode,
    RegNode,
    RootNode,
    SignalNode,
)

# Words that cannot be used as identifiers in either Python or C++.
#
# We deliberately use only ``keyword.kwlist`` (hard Python keywords) and skip
# ``keyword.softkwlist`` -- soft keywords like ``_``, ``match``, ``case``,
# ``type`` are only reserved in specific syntactic contexts (e.g. match-case
# patterns) and remain valid identifiers everywhere else. Mangling them
# would, for example, turn the bare ``_`` field into ``__``, which is more
# disruptive than helpful.
_RESERVED_WORDS: frozenset[str] = frozenset(
    set(keyword.kwlist)
    | {
        # C++ keywords / reserved identifiers that could collide with an RDL
        # inst_name. Not exhaustive -- focused on commonly-used names.
        "alignas",
        "alignof",
        "and",
        "and_eq",
        "asm",
        "auto",
        "bitand",
        "bitor",
        "bool",
        "break",
        "case",
        "catch",
        "char",
        "char8_t",
        "char16_t",
        "char32_t",
        "class",
        "compl",
        "concept",
        "const",
        "consteval",
        "constexpr",
        "constinit",
        "const_cast",
        "continue",
        "co_await",
        "co_return",
        "co_yield",
        "decltype",
        "default",
        "delete",
        "do",
        "double",
        "dynamic_cast",
        "else",
        "enum",
        "explicit",
        "export",
        "extern",
        "false",
        "float",
        "for",
        "friend",
        "goto",
        "if",
        "inline",
        "int",
        "long",
        "mutable",
        "namespace",
        "new",
        "noexcept",
        "not",
        "not_eq",
        "nullptr",
        "operator",
        "or",
        "or_eq",
        "private",
        "protected",
        "public",
        "register",
        "reinterpret_cast",
        "requires",
        "return",
        "short",
        "signed",
        "sizeof",
        "static",
        "static_assert",
        "static_cast",
        "struct",
        "switch",
        "template",
        "this",
        "thread_local",
        "throw",
        "true",
        "try",
        "typedef",
        "typeid",
        "typename",
        "union",
        "unsigned",
        "using",
        "virtual",
        "void",
        "volatile",
        "wchar_t",
        "while",
        "xor",
        "xor_eq",
        # Identifiers used by the generator itself; collisions would shadow
        # generated members.
        "Master",
        "RegisterBase",
        "FieldBase",
        "NodeBase",
        "MemoryBase",
    }
)


[docs] class Nodes(TypedDict): addrmaps: list[AddrmapNode] regfiles: list[RegfileNode] regs: list[RegNode] fields: list[FieldNode] mems: list[MemNode] flag_regs: list[RegNode] enum_regs: list[RegNode] # RDL ``signal`` declarations under any AddrmapNode / RegfileNode. # Consumed by ``runtime.py.jinja`` to populate the per-SoC signal # registry that ``runtime.signals._attach_signals`` reads at # create() time. Signals have no relevant descendants for the # exporter, so the collector never recurses into a SignalNode. signals: list[SignalNode] # Per-register flag/enum members: keyed by id(reg) -> [(name, value), ...]. # Populated for entries in flag_regs and enum_regs. register_members: dict[int, list[tuple[str, int]]] # Per-field RDL ``encode`` enums: keyed by field path -> [(name, value), ...]. # Populated when a FieldNode has a non-None ``encode`` property pointing # at a systemrdl ``UserEnum``. Consumed by ``runtime.py.jinja`` to emit # one ``IntEnum`` per encoded field (sketch §8.1). Orthogonal to the # register-level ``is_flag`` / ``is_enum`` mechanism. # # Keyed by path string (not ``id(field)``) because systemrdl's # ``RegNode.fields()`` returns fresh ``FieldNode`` wrappers every call — # identity is per-iteration, but ``get_path()`` is stable. field_encodes: dict[str, list[tuple[str, int]]]
# UDPs that the exporter understands. CLI users declare these in their RDL # (or call ``Pybind11Exporter.register_udps(rdl_compiler)`` when invoking # the compiler programmatically). # # - is_flag, is_enum (reg, bool) : tag a register as IntFlag / IntEnum # - flag_disable (field, str) : comma-separated list of bit indices # within the field (0 = lsb) to drop # from the generated enum/flag. # - flag_names (field, str) : comma-separated identifiers, mapped # 1:1 to the bits remaining after # flag_disable. Trailing positions # without an entry fall back to the # default "{field}_{i}" naming. _KNOWN_UDPS: tuple[tuple[str, str, type], ...] = ( ("is_flag", "reg", bool), ("is_enum", "reg", bool), ("flag_disable", "field", str), ("flag_names", "field", str), )
[docs] class Pybind11Exporter: """ Export SystemRDL register descriptions to PyBind11 C++ modules """
[docs] def __init__(self) -> None: self.env = Environment( loader=PackageLoader("peakrdl_pybind11", "templates"), autoescape=select_autoescape(), trim_blocks=True, lstrip_blocks=True, ) self.env.filters["pybind_name"] = self._pybind_name_from_node self.env.filters["enum_member"] = self._enum_member_name self.env.filters["cpp_string"] = self._cpp_string_escape # ``repr`` produces a properly-quoted, escape-safe Python literal # for any value — used by the signals block in ``runtime.py.jinja`` # to inline RDL paths and UDP keys without re-implementing escape # logic. self.env.filters["python_string"] = repr self.env.filters["safe_id"] = self._sanitize_identifier # Lazily resolves to the (name, value) list for an is_flag / is_enum # register; populated by _collect_nodes. self._members_by_id: dict[int, list[tuple[str, int]]] = {} self.env.filters["members"] = self._members_for_node # Per-field encode IntEnum member list; populated by _collect_nodes. # Keyed by ``FieldNode.get_path()`` because systemrdl returns fresh # FieldNode wrappers from each ``RegNode.fields()`` call — keying on # ``id(field)`` would miss every template-side lookup. self._field_encodes_by_path: dict[str, list[tuple[str, int]]] = {} self.env.filters["field_encode_members"] = self._field_encode_members_for_node self.soc_name: str | None = None self.soc_version: str = "0.1.0" self.top_node: AddrmapNode | None = None self.output_dir: Path | None = None self._name_cache: dict[str, str] = {} # Discover sibling-unit exporter plugins. Each plugin's # ``register(self)`` runs immediately so it can install Jinja # filters, store references, etc.; codegen-time hooks (if any) # are scheduled by the plugin via attributes on the exporter. from .exporter_plugins import discover_plugins discover_plugins(self)
[docs] def export( self, top_node: RootNode | AddrmapNode, output_dir: str, soc_name: str | None = None, soc_version: str = "0.1.0", gen_pyi: bool = True, split_bindings: int = 100, split_by_hierarchy: bool = False, interrupt_pattern: object | None = None, ) -> None: """ Export SystemRDL to PyBind11 modules Parameters: top_node: Root node of the SystemRDL compilation output_dir: Directory to write output files soc_name: Name of the SoC module (default: derived from top node) soc_version: Version string for the SoC module (default: "0.1.0") gen_pyi: Generate .pyi stub files for type hints split_bindings: Split bindings into multiple files when register count exceeds this threshold. Set to 0 to disable splitting. Default: 100 Ignored when split_by_hierarchy is True. split_by_hierarchy: When True, split bindings by addrmap/regfile hierarchy instead of by register count. This keeps related registers together and provides more logical grouping. Default: False interrupt_pattern: Optional override for the interrupt-state-register matcher used by the feature_detection exporter plugin. Accepts a regex string, compiled ``re.Pattern``, or a callable ``(name: str) -> bool``. """ self.top_node = top_node.top if isinstance(top_node, RootNode) else top_node self.output_dir = Path(output_dir) self.soc_name = soc_name or self.top_node.inst_name or "soc" self.soc_version = soc_version self.split_bindings = split_bindings self.split_by_hierarchy = split_by_hierarchy self.interrupt_pattern = interrupt_pattern # Sanitize soc_name for use as identifier self.soc_name = self._sanitize_identifier(self.soc_name) # Create output directory os.makedirs(output_dir, exist_ok=True) # Collect all nodes first nodes = self._collect_nodes(self.top_node) self._members_by_id = nodes["register_members"] self._field_encodes_by_path = nodes["field_encodes"] # Generate C++ descriptor header self._generate_descriptors(nodes) # Generate PyBind11 bindings (split if needed) self._generate_bindings(nodes) # Generate Python runtime self._generate_python_runtime(nodes) # Generate setup.py for building the module self._generate_setup_py(nodes) # Generate .pyi stub files if requested if gen_pyi: self._generate_pyi_stubs(nodes) # Run post-export plugins (interrupt detection, schema, etc.). # Plugins are best-effort: a plugin failure must not stop the # main exporter from declaring success. self._run_post_export_plugins(nodes)
def _run_post_export_plugins(self, nodes: "Nodes") -> None: from .exporter_plugins import PluginContext, run_post_export assert self.top_node is not None assert self.output_dir is not None assert self.soc_name is not None ctx = PluginContext( exporter=self, top_node=self.top_node, output_dir=self.output_dir, soc_name=self.soc_name, nodes=nodes, options={"interrupt_pattern": self.interrupt_pattern}, ) try: run_post_export(ctx) except Exception: # pragma: no cover - defensive import logging logging.getLogger(__name__).exception("post_export plugin failed") def _sanitize_identifier(self, name: str) -> str: """Sanitize a name to be a valid Python/C++ identifier. Replaces non-identifier characters with underscores, prefixes a leading digit, and appends a trailing underscore to any sanitized name that collides with a Python or C++ reserved word. """ name = re.sub(r"[^a-zA-Z0-9_]", "_", name) if name and name[0].isdigit(): name = "_" + name if name in _RESERVED_WORDS: name = name + "_" return name or "soc" def _pybind_name_from_node(self, value: Node | str) -> str: """Return a unique, sanitized identifier for a node.""" if isinstance(value, Node): path = value.get_path() else: path = str(value) if path not in self._name_cache: sanitized_path = path.replace(".", "__").replace("[", "_").replace("]", "_") self._name_cache[path] = self._sanitize_identifier(sanitized_path) return self._name_cache[path] @staticmethod def _cpp_string_escape(value: object) -> str: """Escape ``value`` so it is safe to embed inside a C++ "..." literal.""" text = "" if value is None else str(value) out: list[str] = [] for ch in text: if ch == "\\": out.append("\\\\") elif ch == '"': out.append('\\"') elif ch == "\n": out.append("\\n") elif ch == "\r": out.append("\\r") elif ch == "\t": out.append("\\t") elif ord(ch) < 0x20: out.append(f"\\x{ord(ch):02x}") else: out.append(ch) return "".join(out) def _enum_member_name(self, name: str) -> str: """Convert a field name into a suitable enum member name.""" parts = re.split(r"[^a-zA-Z0-9]+", name) camel = "".join(part[:1].upper() + part[1:] for part in parts if part) candidate = camel or name candidate = re.sub(r"[^a-zA-Z0-9_]", "_", candidate) if candidate and candidate[0].isdigit(): candidate = "_" + candidate return candidate or "Field" def _generate_descriptors(self, nodes: Nodes) -> None: """Generate C++ descriptor header file""" template = self.env.get_template("descriptors.hpp.jinja") output = template.render( soc_name=self.soc_name, top_node=self.top_node, nodes=nodes, ) assert self.output_dir is not None filepath = self.output_dir / f"{self.soc_name}_descriptors.hpp" with filepath.open("w", encoding="utf-8") as f: f.write(output) def _generate_bindings(self, nodes: Nodes) -> None: """Generate PyBind11 bindings C++ file(s)""" reg_count = len(nodes["regs"]) # Determine split mode if self.split_by_hierarchy: # Split by addrmap/regfile hierarchy self._generate_hierarchical_split_bindings(nodes) elif self.split_bindings > 0 and reg_count > self.split_bindings: # Split by register count self._generate_split_bindings(nodes) else: # Single file self._generate_single_binding(nodes) def _generate_single_binding(self, nodes: Nodes) -> None: """Generate a single bindings file""" template = self.env.get_template("bindings.cpp.jinja") output = template.render( soc_name=self.soc_name, top_node=self.top_node, nodes=nodes, split_mode=False, ) assert self.output_dir is not None filepath = self.output_dir / f"{self.soc_name}_bindings.cpp" with filepath.open("w", encoding="utf-8") as f: f.write(output) def _generate_split_bindings(self, nodes: Nodes) -> None: """Generate multiple split binding files for parallel compilation""" regs = nodes["regs"] chunk_size = self.split_bindings num_chunks = (len(regs) + chunk_size - 1) // chunk_size # Ceiling division # Generate the main module file main_template = self.env.get_template("bindings_main.cpp.jinja") main_output = main_template.render( soc_name=self.soc_name, top_node=self.top_node, num_chunks=num_chunks, nodes=nodes, ) assert self.output_dir is not None filepath = self.output_dir / f"{self.soc_name}_bindings.cpp" with filepath.open("w", encoding="utf-8") as f: f.write(main_output) # Generate split binding files chunk_template = self.env.get_template("bindings_chunk.cpp.jinja") for chunk_idx in range(num_chunks): start_idx = chunk_idx * chunk_size end_idx = min(start_idx + chunk_size, len(regs)) chunk_regs = regs[start_idx:end_idx] chunk_output = chunk_template.render( soc_name=self.soc_name, chunk_idx=chunk_idx, regs=chunk_regs, ) filepath = self.output_dir / f"{self.soc_name}_bindings_{chunk_idx}.cpp" with filepath.open("w", encoding="utf-8") as f: f.write(chunk_output) def _generate_hierarchical_split_bindings(self, nodes: Nodes) -> None: """Generate split binding files organized by addrmap/regfile hierarchy""" # Group registers by their parent addrmap or regfile hierarchy_groups = self._group_registers_by_hierarchy(nodes) if not hierarchy_groups: # Fallback to single binding if no groups self._generate_single_binding(nodes) return num_chunks = len(hierarchy_groups) # Generate the main module file main_template = self.env.get_template("bindings_main.cpp.jinja") main_output = main_template.render( soc_name=self.soc_name, top_node=self.top_node, num_chunks=num_chunks, nodes=nodes, ) assert self.output_dir is not None filepath = self.output_dir / f"{self.soc_name}_bindings.cpp" with filepath.open("w", encoding="utf-8") as f: f.write(main_output) # Generate split binding files for each hierarchy group chunk_template = self.env.get_template("bindings_chunk.cpp.jinja") for chunk_idx, (group_name, group_regs) in enumerate(hierarchy_groups.items()): chunk_output = chunk_template.render( soc_name=self.soc_name, chunk_idx=chunk_idx, regs=group_regs, chunk_name=group_name, # Optional: for documentation/comments ) filepath = self.output_dir / f"{self.soc_name}_bindings_{chunk_idx}.cpp" with filepath.open("w", encoding="utf-8") as f: f.write(chunk_output) def _group_registers_by_hierarchy(self, nodes: Nodes) -> OrderedDict[str, list[RegNode]]: """Group registers by their parent addrmap or regfile for hierarchical splitting This method groups registers based on their immediate parent addrmap or regfile. Priority: regfile > addrmap > top_level Performance: O(n) where n is the number of registers, using a single pass with O(1) lookups. """ from collections import OrderedDict groups: OrderedDict[str, list[RegNode]] = OrderedDict() # Create lookup dictionaries using object id for O(1) lookups # Map id(node) -> node for quick membership testing regfiles_map = {id(rf): rf for rf in nodes["regfiles"]} addrmaps_map = {id(am): am for am in nodes["addrmaps"] if am != self.top_node} # Single pass through all registers to find their grouping parent for reg in nodes["regs"]: # Walk up the hierarchy to find the first regfile or addrmap (excluding top) group_parent = None current: AddrmapNode | MemNode | RegNode | RootNode | None = reg.parent while current is not None: current_id = id(current) # Prioritize regfiles over addrmaps if current_id in regfiles_map: group_parent = current break elif current_id in addrmaps_map: group_parent = current break current = current.parent # Determine the group name and add the register if group_parent is not None: group_name = group_parent.inst_name if group_name not in groups: groups[group_name] = [] groups[group_name].append(reg) else: # Orphan register (direct child of top node or no matching parent) if "top_level" not in groups: groups["top_level"] = [] groups["top_level"].append(reg) return groups def _generate_python_runtime(self, nodes: Nodes) -> None: """Generate Python runtime module""" template = self.env.get_template("runtime.py.jinja") output = template.render( soc_name=self.soc_name, top_node=self.top_node, nodes=nodes, strict_fields=getattr(self, "strict_fields", True), ) assert self.output_dir is not None assert self.soc_name is not None pkg_dir = self.output_dir / self.soc_name pkg_dir.mkdir(exist_ok=True) for filepath in (pkg_dir / "__init__.py", self.output_dir / "__init__.py"): with filepath.open("w", encoding="utf-8") as f: f.write(output) def _generate_setup_py(self, nodes: Nodes) -> None: """Generate CMakeLists.txt and pyproject.toml for building the C++ extension""" reg_count = len(nodes["regs"]) # Determine if we're using split bindings and count the chunks if self.split_by_hierarchy: # Hierarchical splitting hierarchy_groups = self._group_registers_by_hierarchy(nodes) num_chunks = len(hierarchy_groups) if hierarchy_groups else 0 use_split = num_chunks > 0 else: # Register count splitting use_split = self.split_bindings > 0 and reg_count > self.split_bindings if use_split: chunk_size = self.split_bindings num_chunks = (reg_count + chunk_size - 1) // chunk_size else: num_chunks = 0 if use_split and num_chunks > 0: source_files = [f"{self.soc_name}_bindings.cpp"] + [ f"{self.soc_name}_bindings_{i}.cpp" for i in range(num_chunks) ] else: source_files = [f"{self.soc_name}_bindings.cpp"] assert self.output_dir is not None # Generate CMakeLists.txt cmake_template = self.env.get_template("CMakeLists.txt.jinja") cmake_output = cmake_template.render( soc_name=self.soc_name, source_files=source_files, ) cmake_filepath = self.output_dir / "CMakeLists.txt" with cmake_filepath.open("w", encoding="utf-8") as f: f.write(cmake_output) # Generate pyproject.toml for the module pyproject_template = self.env.get_template("pyproject_module.toml.jinja") pyproject_output = pyproject_template.render( soc_name=self.soc_name, soc_version=self.soc_version, ) pyproject_filepath = self.output_dir / "pyproject.toml" with pyproject_filepath.open("w", encoding="utf-8") as f: f.write(pyproject_output) def _generate_pyi_stubs(self, nodes: Nodes) -> None: """Generate .pyi stub files for type hints""" template = self.env.get_template("stubs.pyi.jinja") output = template.render( soc_name=self.soc_name, top_node=self.top_node, nodes=nodes, ) assert self.output_dir is not None assert self.soc_name is not None pkg_dir = self.output_dir / self.soc_name pkg_dir.mkdir(exist_ok=True) for filepath in (pkg_dir / "__init__.pyi", self.output_dir / "__init__.pyi"): with filepath.open("w", encoding="utf-8") as f: f.write(output) def _collect_nodes(self, node: Node, nodes: Nodes | None = None) -> Nodes: """Recursively collect all nodes in the hierarchy""" if nodes is None: nodes = { "addrmaps": [], "regfiles": [], "regs": [], "fields": [], "mems": [], "flag_regs": [], "enum_regs": [], "signals": [], "register_members": {}, "field_encodes": {}, } if isinstance(node, AddrmapNode): nodes["addrmaps"].append(node) for child in node.children(): # ``SignalNode`` children of an addrmap/regfile have no # relevant descendants for us; track them flat and skip # the recursive descent the other kinds use. if isinstance(child, SignalNode): nodes["signals"].append(child) continue self._collect_nodes(child, nodes) elif isinstance(node, RegfileNode): nodes["regfiles"].append(node) for child in node.children(): if isinstance(child, SignalNode): nodes["signals"].append(child) continue self._collect_nodes(child, nodes) elif isinstance(node, MemNode): children = list(node.children()) if children: nodes["mems"].append(node) for child in children: self._collect_nodes(child, nodes) elif isinstance(node, RegNode): nodes["regs"].append(node) is_flag = self._get_bool_property(node, "is_flag") is_enum = self._get_bool_property(node, "is_enum") # If both are set, is_flag takes precedence. if is_flag: nodes["flag_regs"].append(node) nodes["register_members"][id(node)] = self._register_member_layout(node) elif is_enum: nodes["enum_regs"].append(node) nodes["register_members"][id(node)] = self._register_member_layout(node) for field in node.fields(): nodes["fields"].append(field) # Per-field RDL ``encode`` UDP (sketch §8.1). The property # returns a ``UserEnum`` subclass whose ``.members`` is an # ordered dict of ``name -> UserEnumMember``. Coerce to a # ``(name, int)`` list once so the template doesn't need # to know about systemrdl internals. try: enc = field.get_property("encode") except LookupError: enc = None if enc is not None: members = getattr(enc, "members", None) if members: nodes["field_encodes"][field.get_path()] = [ (str(name), int(member.value)) for name, member in members.items() ] return nodes def _get_bool_property(self, node: Node, name: str) -> bool: """Safely read a boolean property from a node.""" try: value = node.get_property(name) except LookupError: return False return bool(value) def _members_for_node(self, node: Node) -> list[tuple[str, int]]: """Jinja filter: return the (name, value) members for a flag/enum reg.""" return self._members_by_id.get(id(node), []) def _field_encode_members_for_node(self, node: Node) -> list[tuple[str, int]]: """Jinja filter: return the IntEnum members for a field with RDL ``encode``. Returns ``[]`` when the field has no encode — that's the signal the template uses to decide whether to emit the per-field enum class. Lookup is by ``node.get_path()`` because systemrdl's ``RegNode.fields()`` returns fresh FieldNode wrappers per call; keying on ``id(node)`` would always miss the template-side lookups. """ return self._field_encodes_by_path.get(node.get_path(), []) @staticmethod def _get_string_property(node: Node, name: str) -> str | None: """Safely read a string property from a node, returning None if absent.""" try: value = node.get_property(name) except LookupError: return None if value is None or value == "": return None return str(value) @staticmethod def _parse_index_list(value: str, *, width: int, where: str) -> set[int]: """Parse a comma-separated bit-index list and validate against ``width``.""" result: set[int] = set() for token in value.split(","): token = token.strip() if not token: continue try: idx = int(token, 0) except ValueError as e: raise ValueError(f"{where}: cannot parse {token!r} as an integer") from e if idx < 0 or idx >= width: raise ValueError(f"{where}: bit index {idx} is out of range for a width-{width} field") result.add(idx) return result @staticmethod def _parse_name_list(value: str) -> list[str]: """Parse a comma-separated identifier list, dropping empty entries.""" return [s.strip() for s in value.split(",") if s.strip()] def _register_member_layout(self, reg: RegNode) -> list[tuple[str, int]]: """Compute (name, value) pairs for an is_flag/is_enum register. Each field contributes one member per *enabled* bit position. The ``flag_disable`` field UDP drops bit positions (indices 0..width-1, where 0 is the field's lsb) before naming. The ``flag_names`` field UDP supplies explicit identifiers mapped 1:1 to the remaining bits in ascending order; trailing positions without an entry fall back to ``{field}_{i}`` (where i is the bit-position-within-field). """ members: list[tuple[str, int]] = [] for field in reg.fields(): width = field.width low = field.low disable_str = self._get_string_property(field, "flag_disable") if disable_str: disabled = self._parse_index_list( disable_str, width=width, where=f"flag_disable on {field.get_path()}" ) else: disabled = set() enabled = [i for i in range(width) if i not in disabled] names_str = self._get_string_property(field, "flag_names") if names_str: names = self._parse_name_list(names_str) if len(names) > len(enabled): raise ValueError( f"flag_names on {field.get_path()} has {len(names)} entries " f"but only {len(enabled)} bit(s) remain after flag_disable" ) else: names = [] base_name = self._sanitize_identifier(field.inst_name) for slot, bit_index in enumerate(enabled): if slot < len(names): name = self._sanitize_identifier(names[slot]) elif width == 1: name = base_name else: name = f"{base_name}_{bit_index}" members.append((name, 1 << (low + bit_index))) return members
[docs] @classmethod def register_udps(cls, rdl_compiler: object) -> None: """Register every UDP this exporter recognizes with the given compiler. For programmatic use: from systemrdl import RDLCompiler from peakrdl_pybind11 import Pybind11Exporter rdl = RDLCompiler() Pybind11Exporter.register_udps(rdl) rdl.compile_file(...) CLI users can equivalently declare the UDPs in their RDL. """ for prop_name, component, prop_type in _KNOWN_UDPS: cls._register_udp(rdl_compiler, prop_name, component, prop_type)
@staticmethod def _register_udp(rdl_compiler: object, prop_name: str, component: str, prop_type: type) -> None: from systemrdl import component as _comp from systemrdl.udp import UDPDefinition component_cls_map = { "reg": _comp.Reg, "field": _comp.Field, } try: comp_cls = component_cls_map[component] except KeyError as e: raise ValueError(f"Unsupported UDP component scope: {component!r}") from e udp_class = type( f"_UDPDef_{prop_name}", (UDPDefinition,), { "name": prop_name, "valid_components": {comp_cls}, "valid_type": prop_type, }, ) register = getattr(rdl_compiler, "register_udp", None) if register is None: raise TypeError( "rdl_compiler does not look like a systemrdl RDLCompiler (no register_udp method)" ) # soft=False so the UDP is recognized immediately without the user # also having to declare `property is_flag { ... };` in their RDL. register(udp_class, soft=False)