Source code for FABulous.fabric_definition.Yosys_obj

"""Object representation of the Yosys Json file."""

import json
import re
import subprocess
import tempfile
from dataclasses import dataclass, field
from pathlib import Path
from typing import Literal

from FABulous.custom_exception import InvalidFileType
from FABulous.FABulous_settings import get_context

"""
Type alias for Yosys bit vectors containing integers or logic values.

BitVector represents signal values in Yosys netlists as lists containing
integers (for signal IDs) or logic state strings ("0", "1", "x", "z").
"""
[docs] BitVector = list[int | Literal["0", "1", "x", "z"]]
[docs] KeyValue = dict[str, str | int]
@dataclass
[docs] class YosysPortDetails: """Represents port details in a Yosys module. Attributes ---------- direction : {"input", "output", "inout"} Port direction. bits : BitVector Bit vector representing the port's signals. offset : int, default 0 Bit offset for multi-bit ports. upto : int, default 0 Upper bound for bit ranges. signed : int, default 0 Whether the port is signed (0=unsigned, 1=signed). """
[docs] direction: Literal["input", "output", "inout"]
[docs] bits: BitVector
[docs] offset: int = 0
[docs] upto: int = 0
[docs] signed: int = 0
@dataclass
[docs] class YosysCellDetails: """Represents a cell instance in a Yosys module. Cells are instantiated components like logic gates, flip-flops, or user-defined modules. Attributes ---------- hide_name : {0, 1} Whether to hide the cell name in output (1=hide, 0=show). type : str Cell type/primitive name (e.g., "AND", "DFF", custom module name). parameters : dict[str, str] Cell parameters as string key-value pairs. attributes : dict[str, str | int] Cell attributes including metadata and synthesis directives. connections : dict[str, BitVector] Port connections mapping port names to bit vectors. port_directions : dict[str, {"input", "output", "inout"}], optional Direction of each port. Default is empty dict. model : str, optional Associated model name. Default is "". """
[docs] hide_name: Literal[1, 0]
[docs] type: str
[docs] parameters: KeyValue
[docs] attributes: KeyValue
[docs] connections: dict[str, BitVector]
[docs] port_directions: dict[str, Literal["input", "output", "inout"]] = field( default_factory=dict )
[docs] model: str = ""
@dataclass
[docs] class YosysMemoryDetails: """Represents memory block details in a Yosys module. Memory blocks are inferred or explicitly instantiated memory elements. Attributes ---------- hide_name : {0, 1} Whether to hide the memory name in output (1=hide, 0=show). attributes : dict[str, str] Memory attributes and metadata. width : int Data width in bits. start_offset : int Starting address offset. size : int Memory size (number of addressable locations). """
[docs] hide_name: Literal[1, 0]
[docs] attributes: KeyValue
[docs] width: int
[docs] start_offset: int
[docs] size: int
@dataclass
[docs] class YosysNetDetails: """Represents net/wire details in a Yosys module. Nets are the connections between cells and ports in the design. Attributes ---------- hide_name : {0, 1} Whether to hide the net name in output (1=hide, 0=show). bits : BitVector Bit vector representing the net's signals. attributes : dict[str, str] Net attributes including unused bit information. offset : int, default 0 Bit offset for multi-bit nets. upto : int, default 0 Upper bound for bit ranges. signed : int, default 0 Whether the net is signed (0=unsigned, 1=signed). """
[docs] hide_name: Literal[1, 0]
[docs] bits: BitVector
[docs] attributes: KeyValue
[docs] offset: int = 0
[docs] upto: int = 0
[docs] signed: int = 0
@dataclass
[docs] class YosysModule: """Represents a module in a Yosys design. A module contains the structural description of a digital circuit including its interface (ports), internal components (cells), memory blocks, and interconnections (nets). Attributes ---------- attributes : dict[str, str | int] Module attributes and metadata (e.g., "top" for top module). parameter_default_values : dict[str, str | int] Default values for module parameters. ports : dict[str, YosysPortDetails] Dictionary mapping port names to YosysPortDetails. cells : dict[str, YosysCellDetails] Dictionary mapping cell names to YosysCellDetails. memories : dict[str, YosysMemoryDetails] Dictionary mapping memory names to YosysMemoryDetails. netnames : dict[str, YosysNetDetails] Dictionary mapping net names to YosysNetDetails. """
[docs] attributes: KeyValue
[docs] parameter_default_values: KeyValue
[docs] ports: dict[str, YosysPortDetails]
[docs] cells: dict[str, YosysCellDetails]
[docs] memories: dict[str, YosysMemoryDetails]
[docs] netnames: dict[str, YosysNetDetails]
def __init__( self, *, attributes: KeyValue, parameter_default_values: KeyValue, ports: dict[str, YosysPortDetails], cells: dict[str, YosysCellDetails], memories: dict[str, YosysMemoryDetails], netnames: dict[str, YosysNetDetails], ) -> None: """Initialize a YosysModule from parsed JSON data. Parameters ---------- attributes : dict Module attributes dictionary. parameter_default_values : dict Parameter defaults dictionary. ports : dict Ports dictionary (will be converted to YosysPortDetails objects). cells : dict Cells dictionary (will be converted to YosysCellDetails objects). memories : dict Memories dictionary (will be converted to YosysMemoryDetails objects). netnames : dict Netnames dictionary (will be converted to YosysNetDetails objects). """ self.attributes = attributes self.parameter_default_values = parameter_default_values self.ports = {k: YosysPortDetails(**v) for k, v in ports.items()} self.cells = {k: YosysCellDetails(**v) for k, v in cells.items()} self.memories = {k: YosysMemoryDetails(**v) for k, v in memories.items()} self.netnames = {k: YosysNetDetails(**v) for k, v in netnames.items()}
@dataclass
[docs] class YosysJson: """Root object representing a complete Yosys JSON file. This class provides the main interface for loading and analyzing Yosys JSON netlists. It contains all modules in the design and provides utility methods for common netlist analysis tasks. Attributes ---------- srcPath : Path Path to the source JSON file. creator : str Tool that created the JSON (usually "Yosys"). modules : dict[str, YosysModule] Dictionary mapping module names to YosysModule objects. models : dict Dictionary of behavioral models (implementation-specific). """
[docs] srcPath: Path
[docs] creator: str
[docs] modules: dict[str, YosysModule]
[docs] models: dict
def __init__(self, path: Path) -> None: """Load and parse a HDL file to a Yosys JSON object. Parameters ---------- path : Path Path to a HDL file. Raises ------ FileNotFoundError If the JSON file doesn't exist. json.JSONDecodeError If the file contains invalid JSON. ValueError If the HDL file type is unsupported. """ if not path.exists(): raise FileNotFoundError(f"File {path} does not exist") if path.suffix not in {".vhd", ".vhdl", ".v", ".sv"}: raise InvalidFileType( f"Unsupported HDL file type: {path.suffix}. " f"Supported types are .vhd, .vhdl, .v, .sv" ) self.srcPath = path yosys = get_context().yosys_path ghdl = get_context().ghdl_path json_file = self.srcPath.with_suffix(".json") # FIXME: a fake file to ensure things working with 1.3 temp: Path = Path(tempfile.gettempdir()) temp = temp / "my_package.vhd" temp.touch() temp.write_text("package my_package is\nend package;\n") if self.srcPath.suffix in {".vhd", ".vhdl"}: runCmd = [ f"{ghdl!s}", "--synth", "--std=08", "--out=verilog", str(temp), f"{get_context().model_pack!s}", f"{self.srcPath}", "-e", f"{self.srcPath.stem}", ] try: r = subprocess.run(runCmd, check=True, capture_output=True) self.srcPath.with_suffix(".v").write_text(r.stdout.decode()) except subprocess.CalledProcessError as e: raise RuntimeError( f"Failed to run GHDL on {self.srcPath}: {e.stderr.decode()} " f"run cmd: {' '.join(runCmd)}" ) from e runCmd = [ f"{yosys!s}", "-q", ( "-p " f"read_verilog -sv {self.srcPath.with_suffix('.v')}; " "hierarchy -auto-top; " "proc -noopt; " f"write_json -compat-int {json_file}" ), ] try: subprocess.run(runCmd, check=True, capture_output=True) except subprocess.CalledProcessError as e: raise RuntimeError( f"Failed to run Yosys on {self.srcPath}: {e.stderr.decode()}" ) from e with json_file.open() as f: o = json.load(f) self.creator = o.get("creator", "") # Use .get() for safety # Provide default empty dicts for potentially missing keys in module data self.modules = { k: YosysModule( attributes=v.get("attributes", {}), parameter_default_values=v.get("parameter_default_values", {}), ports=v.get("ports", {}), cells=v.get("cells", {}), memories=v.get("memories", {}), # Provide default for memories netnames=v.get("netnames", {}), # Provide default for netnames ) for k, v in o.get("modules", {}).items() # Use .get() for safety } self.models = o.get("models", {}) # Use .get() for safety # Post-process VHDL file for now. Once VHDL is updated, we can remove this. if self.srcPath.suffix in [".vhd", ".vhdl"]: vhdl_content = self.srcPath.read_text() if r := re.search(r"entity\s+(\w+)\s+is", vhdl_content): module_name = r.group(1) else: raise ValueError(f"Could not find entity name in {self.srcPath}") module = self.modules.get(module_name) if not module: raise ValueError( f"Module {module_name} not found in Yosys JSON for {self.srcPath}" ) if r := re.search(r"\(\*.*?BelMap(.*?) \*\)", vhdl_content): res = r.group(1).split(",") res = [x.strip() for x in res] res = [x for x in res if x] # Remove empty strings res = dict(x.split("=", 1) for x in res) module.attributes.update(res) module.attributes["BelMap"] = True module.attributes["FABulous"] = True # because yosys reverses the order of attributes, we need to do the same module.attributes = dict(reversed(list(module.attributes.items()))) ports_entry = [] port_start = False for i in vhdl_content.splitlines(): if re.search(r"^\s*port \(", i, flags=re.IGNORECASE): port_start = True if port_start and re.search(r"^\s*\);\s*", i): port_start = False if port_start: ports_entry.append(i) for p in ports_entry: if r := re.search(r"(\w+)\s*:.*? --\s*\(\* (.*?) \*\)", p): port_name = r.group(1) attribute_entries = r.group(2).split(",") module.netnames[port_name].attributes.update( {x.strip(): 1 for x in attribute_entries} ) if r := re.search(r"(\w+)\s*:.*? --.*", p): port_name = r.group(1) if "EXTERNAL" in p: module.netnames[port_name].attributes["EXTERNAL"] = 1 if "SHARED_PORT" in p: module.netnames[port_name].attributes["SHARED_PORT"] = 1 if "GLOBAL" in p: module.netnames[port_name].attributes["GLOBAL"] = 1
[docs] def getTopModule(self) -> tuple[str, YosysModule]: """Find and return the top-level module in the design. The top module is identified by having a "top" attribute. Returns ------- tuple[str, YosysModule] A tuple containing: - The name of the top-level module (str) - The YosysModule object for the top-level module Raises ------ ValueError If no top module is found in the design. """ for name, module in self.modules.items(): if "top" in module.attributes: return name, module raise ValueError("No top module found in Yosys JSON")
[docs] def isTopModuleNet(self, net: int) -> bool: """Check if a net ID corresponds to a top-level module port. Parameters ---------- net : int Net ID to check. Returns ------- bool True if the net is connected to a top module port, False otherwise. """ for module in self.modules.values(): for pDetail in module.ports.values(): if net in pDetail.bits: return True return False
[docs] def getNetPortSrcSinks( self, net: int ) -> tuple[tuple[str, str], list[tuple[str, str]]]: """Find the source and sink connections for a given net. This method analyzes the netlist to determine what drives a net (source) and what it connects to (sinks). Parameters ---------- net : int Net ID to analyze. Returns ------- tuple[tuple[str, str], list[tuple[str, str]]] A tuple containing: - Source: (cell_name, port_name) tuple for the driving cell/port - Sinks: List of (cell_name, port_name) tuples for driven cells/ports Raises ------ ValueError If net is not found or has multiple drivers. Notes ----- If no driver is found, the source will be ("", "z") indicating a high-impedance or undriven net. """ src: list[tuple[str, str]] = [] sinks: list[tuple[str, str]] = [] for module in self.modules.values(): for cell_name, cell_details in module.cells.items(): for conn_name, conn_details in cell_details.connections.items(): if net in conn_details: if cell_details.port_directions[conn_name] == "output": src.append((cell_name, conn_name)) else: sinks.append((cell_name, conn_name)) if len(sinks) == 0: raise ValueError( f"Net {net} not found in Yosys JSON or is a top module port output" ) if len(src) == 0: src.append(("", "z")) if len(src) > 1: raise ValueError(f"Multiple driver found for net {net}: {src}") return src[0], sinks