Fix nonconstant index errors in Questa by using unpacked structs and interface array intermediates (#17)

* Initial plan

* Fix nonconstant index error by using unpacked structs and arrays

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* Add comprehensive tests for Questa compatibility with instance arrays

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* Add intermediate signals for interface array fanin to fix Questa compatibility

Questa simulator rejects indexing interface arrays with non-constant variables in procedural blocks (always_comb). This fix creates intermediate unpacked array signals that are assigned from interface arrays using generate loops (with genvar), then references those intermediates in the fanin logic.

Changes:
- Added fanin_intermediate_gen.py to generate intermediate signals
- Modified APB3/APB4/AXI4-Lite cpuif classes to use intermediates for interface arrays
- Updated templates to include intermediate signal section
- Intermediate signals use genvar indexing (legal) instead of variable indexing (illegal in Questa)

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* Fix type checking and formatting issues in fanin intermediate generator

- Added proper null checks for node.array_dimensions to avoid type errors
- Used getattr() to safely access is_interface and _interface attributes
- Added early returns when interface is None to prevent errors
- Fixed formatting issues in fanin_intermediate_gen.py
- All type checks now pass (only pre-existing errors remain in other files)
- All 67 tests still pass

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* Fix test collection when cocotb is not installed

The cocotb test files (test_runner.py) have imports that fail at module level when cocotb is not installed, causing pytest collection to fail. This prevents running tests in environments without cocotb dependencies.

Solution: Updated conftest.py to conditionally ignore cocotb tests when cocotb is not available, allowing tests to pass in both environments (with and without cocotb).

- When cocotb is available: only ignore test_register_access.py files (existing behavior)
- When cocotb is not available: ignore all files under cocotb/** directories

This allows CI to run all 67 non-simulation tests successfully without requiring cocotb/verilator dependencies for basic testing.

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* add default for unpacked struct

* cocotb is test dependancy

* Refactor fanin intermediate signal logic to cpuif level

Moved the interface-specific signal assignment logic from fanin_intermediate_gen.py to individual cpuif classes (APB3Cpuif, APB4Cpuif, AXI4LiteCpuif). This follows better architecture principles where each cpuif knows which signals it needs.

Changes:
- Added fanin_intermediate_assignments() method to BaseCpuif
- Implemented fanin_intermediate_assignments() in APB3Cpuif, APB4Cpuif, and AXI4LiteCpuif
- Updated FaninIntermediateGenerator to call the cpuif method instead of checking interface type
- Removed interface type checking logic from fanin_intermediate_gen.py

This makes the code more maintainable and follows the single responsibility principle - each cpuif class knows its own signal requirements.

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
This commit is contained in:
Copilot
2025-10-28 22:03:57 -07:00
committed by GitHub
parent f829e3894f
commit 3d823572cc
15 changed files with 420 additions and 21 deletions

View File

@@ -52,8 +52,16 @@ class APB3Cpuif(BaseCpuif):
fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0"
else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
# Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_ack"] = f"{node.inst_name}_fanin_ready{array_idx}"
fanin["cpuif_rd_err"] = f"{node.inst_name}_fanin_err{array_idx}"
else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
@@ -62,6 +70,23 @@ class APB3Cpuif(BaseCpuif):
if node is None:
fanin["cpuif_rd_data"] = "'0"
else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node, "i")
# Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_data"] = f"{node.inst_name}_fanin_data{array_idx}"
else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for APB3 interface arrays."""
return [
f"assign {inst_name}_fanin_ready{array_idx} = {master_prefix}{indexed_path}.PREADY;",
f"assign {inst_name}_fanin_err{array_idx} = {master_prefix}{indexed_path}.PSLVERR;",
f"assign {inst_name}_fanin_data{array_idx} = {master_prefix}{indexed_path}.PRDATA;",
]

View File

@@ -26,6 +26,13 @@ assign {{cpuif.signal("PSLVERR")}} = cpuif_rd_err | cpuif_rd_sel.cpuif_err | cpu
// Fanout CPU Bus interface signals
//--------------------------------------------------------------------------
{{fanout|walk(cpuif=cpuif)}}
{%- if cpuif.is_interface %}
//--------------------------------------------------------------------------
// Intermediate signals for interface array fanin
//--------------------------------------------------------------------------
{{fanin_intermediate|walk(cpuif=cpuif)}}
{%- endif %}
//--------------------------------------------------------------------------
// Fanin CPU Bus interface signals

View File

@@ -55,8 +55,16 @@ class APB4Cpuif(BaseCpuif):
fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0"
else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
# Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_ack"] = f"{node.inst_name}_fanin_ready{array_idx}"
fanin["cpuif_rd_err"] = f"{node.inst_name}_fanin_err{array_idx}"
else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
@@ -65,6 +73,23 @@ class APB4Cpuif(BaseCpuif):
if node is None:
fanin["cpuif_rd_data"] = "'0"
else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node, "i")
# Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_data"] = f"{node.inst_name}_fanin_data{array_idx}"
else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for APB4 interface arrays."""
return [
f"assign {inst_name}_fanin_ready{array_idx} = {master_prefix}{indexed_path}.PREADY;",
f"assign {inst_name}_fanin_err{array_idx} = {master_prefix}{indexed_path}.PSLVERR;",
f"assign {inst_name}_fanin_data{array_idx} = {master_prefix}{indexed_path}.PRDATA;",
]

View File

@@ -29,6 +29,13 @@ assign {{cpuif.signal("PSLVERR")}} = cpuif_rd_err | cpuif_rd_sel.cpuif_err | cpu
// Fanout CPU Bus interface signals
//--------------------------------------------------------------------------
{{fanout|walk(cpuif=cpuif)}}
{%- if cpuif.is_interface %}
//--------------------------------------------------------------------------
// Intermediate signals for interface array fanin
//--------------------------------------------------------------------------
{{fanin_intermediate|walk(cpuif=cpuif)}}
{%- endif %}
//--------------------------------------------------------------------------
// Fanin CPU Bus interface signals

View File

@@ -4,14 +4,14 @@ from systemrdl.node import AddressableNode
from ...utils import get_indexed_path
from ..base_cpuif import BaseCpuif
from .axi4lite_interface import AXI4LiteSVInterface
from .axi4_lite_interface import AXI4LiteSVInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
class AXI4LiteCpuif(BaseCpuif):
template_path = "axi4lite_tmpl.sv"
template_path = "axi4_lite_tmpl.sv"
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)
@@ -68,9 +68,17 @@ class AXI4LiteCpuif(BaseCpuif):
fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0"
else:
# Read side: ack comes from RVALID; err if RRESP[1] is set (SLVERR/DECERR)
fanin["cpuif_rd_ack"] = self.signal("RVALID", node, "i")
fanin["cpuif_rd_err"] = f"{self.signal('RRESP', node, 'i')}[1]"
# Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_ack"] = f"{node.inst_name}_fanin_ready{array_idx}"
fanin["cpuif_rd_err"] = f"{node.inst_name}_fanin_err{array_idx}"
else:
# Read side: ack comes from RVALID; err if RRESP[1] is set (SLVERR/DECERR)
fanin["cpuif_rd_ack"] = self.signal("RVALID", node, "i")
fanin["cpuif_rd_err"] = f"{self.signal('RRESP', node, 'i')}[1]"
return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items())
@@ -79,6 +87,23 @@ class AXI4LiteCpuif(BaseCpuif):
if node is None:
fanin["cpuif_rd_data"] = "'0"
else:
fanin["cpuif_rd_data"] = self.signal("RDATA", node, "i")
# Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_data"] = f"{node.inst_name}_fanin_data{array_idx}"
else:
fanin["cpuif_rd_data"] = self.signal("RDATA", node, "i")
return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items())
def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for AXI4-Lite interface arrays."""
return [
f"assign {inst_name}_fanin_ready{array_idx} = {master_prefix}{indexed_path}.RVALID;",
f"assign {inst_name}_fanin_err{array_idx} = {master_prefix}{indexed_path}.RRESP[1];",
f"assign {inst_name}_fanin_data{array_idx} = {master_prefix}{indexed_path}.RDATA;",
]

View File

@@ -4,7 +4,7 @@ from systemrdl.node import AddressableNode
from ...utils import get_indexed_path
from ..base_cpuif import BaseCpuif
from .axi4lite_interface import AXI4LiteFlatInterface
from .axi4_lite_interface import AXI4LiteFlatInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
class AXI4LiteCpuifFlat(BaseCpuif):
"""Verilator-friendly variant that flattens the AXI4-Lite interface ports."""
template_path = "axi4lite_tmpl.sv"
template_path = "axi4_lite_tmpl.sv"
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)

View File

@@ -53,6 +53,13 @@ assign {{cpuif.signal("BRESP")}} = (cpuif_wr_err | cpuif_wr_sel.cpuif_err | cpu
// Fanout CPU Bus interface signals
//--------------------------------------------------------------------------
{{fanout|walk(cpuif=cpuif)}}
{%- if cpuif.is_interface %}
//--------------------------------------------------------------------------
// Intermediate signals for interface array fanin
//--------------------------------------------------------------------------
{{fanin_intermediate|walk(cpuif=cpuif)}}
{%- endif %}
//--------------------------------------------------------------------------
// Fanin CPU Bus interface signals

View File

@@ -7,6 +7,7 @@ from systemrdl.node import AddressableNode
from ..utils import clog2, get_indexed_path, is_pow2, roundup_pow2
from .fanin_gen import FaninGenerator
from .fanin_intermediate_gen import FaninIntermediateGenerator
from .fanout_gen import FanoutGenerator
if TYPE_CHECKING:
@@ -97,6 +98,7 @@ class BaseCpuif:
"ds": self.exp.ds,
"fanout": FanoutGenerator,
"fanin": FaninGenerator,
"fanin_intermediate": FaninIntermediateGenerator,
}
template = jj_env.get_template(self.template_path)
@@ -116,3 +118,24 @@ class BaseCpuif:
def readback(self, node: AddressableNode | None = None) -> str:
raise NotImplementedError
def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for interface array fanin.
This method should be implemented by cpuif classes that use interfaces.
It returns a list of assignment strings that copy signals from interface
arrays to intermediate unpacked arrays using constant (genvar) indexing.
Args:
node: The addressable node
inst_name: Instance name for the intermediate signals
array_idx: Array index string (e.g., "[gi0][gi1]")
master_prefix: Master interface prefix
indexed_path: Indexed path to the interface element
Returns:
List of assignment strings
"""
return [] # Default: no intermediate assignments needed

View File

@@ -0,0 +1,142 @@
"""Generator for intermediate signals needed for interface array fanin.
When using SystemVerilog interface arrays, we cannot use variable indices
in procedural blocks (like always_comb). This generator creates intermediate
signals that copy from interface arrays using generate loops, which can then
be safely accessed with variable indices in the fanin logic.
"""
from collections import deque
from typing import TYPE_CHECKING
from systemrdl.node import AddressableNode
from systemrdl.walker import WalkerAction
from ..body import Body, ForLoopBody
from ..design_state import DesignState
from ..listener import BusDecoderListener
from ..utils import get_indexed_path
if TYPE_CHECKING:
from .base_cpuif import BaseCpuif
class FaninIntermediateGenerator(BusDecoderListener):
"""Generates intermediate signals for interface array fanin."""
def __init__(self, ds: DesignState, cpuif: "BaseCpuif") -> None:
super().__init__(ds)
self._cpuif = cpuif
self._declarations: list[str] = []
self._stack: deque[Body] = deque()
self._stack.append(Body())
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
action = super().enter_AddressableComponent(node)
# Only generate intermediates for interface arrays
# Check if cpuif has is_interface attribute (some implementations don't)
is_interface = getattr(self._cpuif, "is_interface", False)
if not is_interface or not node.array_dimensions:
return action
# Generate intermediate signal declarations
self._generate_intermediate_declarations(node)
# Generate assignment logic using generate loops
if node.array_dimensions:
for i, dim in enumerate(node.array_dimensions):
fb = ForLoopBody(
"genvar",
f"gi{i}",
dim,
)
self._stack.append(fb)
# Generate assignments from interface array to intermediates
self._stack[-1] += self._generate_intermediate_assignments(node)
return action
def exit_AddressableComponent(self, node: AddressableNode) -> None:
is_interface = getattr(self._cpuif, "is_interface", False)
if is_interface and node.array_dimensions:
for _ in node.array_dimensions:
b = self._stack.pop()
if not b:
continue
self._stack[-1] += b
super().exit_AddressableComponent(node)
def _generate_intermediate_declarations(self, node: AddressableNode) -> None:
"""Generate intermediate signal declarations for a node."""
inst_name = node.inst_name
# Array dimensions should be checked before calling this function
if not node.array_dimensions:
return
# Calculate total array size
array_size = 1
for dim in node.array_dimensions:
array_size *= dim
# Create array dimension string
array_str = "".join(f"[{dim}]" for dim in node.array_dimensions)
# Generate declarations for each fanin signal
# For APB3/4: PREADY, PSLVERR, PRDATA
# These are the signals read in fanin
self._declarations.append(f"logic {inst_name}_fanin_ready{array_str};")
self._declarations.append(f"logic {inst_name}_fanin_err{array_str};")
self._declarations.append(
f"logic [{self._cpuif.data_width - 1}:0] {inst_name}_fanin_data{array_str};"
)
def _generate_intermediate_assignments(self, node: AddressableNode) -> str:
"""Generate assignments from interface array to intermediate signals."""
inst_name = node.inst_name
indexed_path = get_indexed_path(node.parent, node, "gi", skip_kw_filter=True)
# Get master prefix - use getattr to avoid type errors
interface = getattr(self._cpuif, "_interface", None)
if interface is None:
return ""
master_prefix = interface.get_master_prefix()
# Array dimensions should be checked before calling this function
if not node.array_dimensions:
return ""
# Create indexed signal names for left-hand side
array_idx = "".join(f"[gi{i}]" for i in range(len(node.array_dimensions)))
# Delegate to cpuif to get the appropriate assignments for this interface type
assignments = self._cpuif.fanin_intermediate_assignments(
node, inst_name, array_idx, master_prefix, indexed_path
)
return "\n".join(assignments)
def get_declarations(self) -> str:
"""Get all intermediate signal declarations."""
if not self._declarations:
return ""
return "\n".join(self._declarations)
def __str__(self) -> str:
"""Get all intermediate signal declarations and assignments."""
if not self._declarations:
return ""
# Output declarations first
output = "\n".join(self._declarations)
output += "\n\n"
# Then output assignments
body_str = "\n".join(map(str, self._stack))
if body_str and body_str.strip():
output += body_str
return output

View File

@@ -51,7 +51,7 @@ module {{ds.module_name}}
//--------------------------------------------------------------------------
always_comb begin
// Default all write select signals to 0
cpuif_wr_sel = '0;
cpuif_wr_sel = '{default: '0};
if (cpuif_req && cpuif_wr_en) begin
// A write request is pending
@@ -66,7 +66,7 @@ module {{ds.module_name}}
//--------------------------------------------------------------------------
always_comb begin
// Default all read select signals to 0
cpuif_rd_sel = '0;
cpuif_rd_sel = '{default: '0};
if (cpuif_req && cpuif_rd_en) begin
// A read request is pending

View File

@@ -17,7 +17,7 @@ class StructGenerator(BusDecoderListener):
super().__init__(ds)
self._stack: deque[Body] = deque()
self._stack.append(StructBody("cpuif_sel_t", True, True))
self._stack.append(StructBody("cpuif_sel_t", True, False))
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
action = super().enter_AddressableComponent(node)
@@ -28,7 +28,7 @@ class StructGenerator(BusDecoderListener):
if node.children():
# Push new body onto stack
body = StructBody(f"cpuif_sel_{node.inst_name}_t", True, True)
body = StructBody(f"cpuif_sel_{node.inst_name}_t", True, False)
self._stack.append(body)
return action
@@ -46,7 +46,7 @@ class StructGenerator(BusDecoderListener):
if node.array_dimensions:
for dim in node.array_dimensions:
name = f"[{dim - 1}:0]{name}"
name = f"{name}[{dim}]"
self._stack[-1] += f"{type} {name};"

View File

@@ -0,0 +1,131 @@
"""Test Questa simulator compatibility for instance arrays."""
from collections.abc import Callable
from pathlib import Path
from tempfile import TemporaryDirectory
from systemrdl.node import AddrmapNode
from peakrdl_busdecoder import BusDecoderExporter
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
def test_instance_array_questa_compatibility(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that instance arrays generate Questa-compatible code.
This test ensures that:
- Struct members for arrays use unpacked array syntax (name[dim])
- NOT packed bit-vector syntax ([dim-1:0]name)
- Struct is unpacked (not packed)
- Array indexing with loop variables works correctly
This fixes the error: "Nonconstant index into instance array"
"""
rdl_source = """
addrmap test_map {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg[4] @ 0x0 += 0x10;
};
"""
top = compile_rdl(rdl_source, top="test_map")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Read the generated module
module_file = Path(tmpdir) / "test_map.sv"
content = module_file.read_text()
# Should use unpacked struct
assert "typedef struct {" in content
assert "typedef struct packed" not in content
# Should use unpacked array syntax for array members
assert "logic my_reg[4];" in content
# Should NOT use packed bit-vector syntax
assert "[3:0]my_reg" not in content
# Should have proper array indexing in decode logic
assert "cpuif_wr_sel.my_reg[i0] = 1'b1;" in content
assert "cpuif_rd_sel.my_reg[i0] = 1'b1;" in content
# Should have proper array indexing in fanout/fanin logic
assert "cpuif_wr_sel.my_reg[gi0]" in content or "cpuif_rd_sel.my_reg[gi0]" in content
assert "cpuif_wr_sel.my_reg[i0]" in content or "cpuif_rd_sel.my_reg[i0]" in content
def test_multidimensional_array_questa_compatibility(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that multidimensional instance arrays generate Questa-compatible code."""
rdl_source = """
addrmap test_map {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg[2][3] @ 0x0 += 0x10;
};
"""
top = compile_rdl(rdl_source, top="test_map")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Read the generated module
module_file = Path(tmpdir) / "test_map.sv"
content = module_file.read_text()
# Should use unpacked struct with multidimensional array
assert "typedef struct {" in content
# Should use unpacked array syntax for multidimensional arrays
assert "logic my_reg[2][3];" in content
# Should NOT use packed bit-vector syntax
assert "[1:0][2:0]my_reg" not in content
assert "[5:0]my_reg" not in content
def test_nested_instance_array_questa_compatibility(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that nested instance arrays generate Questa-compatible code."""
rdl_source = """
addrmap inner_map {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} inner_reg[2] @ 0x0 += 0x10;
};
addrmap outer_map {
inner_map inner[3] @ 0x0 += 0x100;
};
"""
top = compile_rdl(rdl_source, top="outer_map")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Read the generated module
module_file = Path(tmpdir) / "outer_map.sv"
content = module_file.read_text()
# Should use unpacked struct
assert "typedef struct {" in content
# Inner should be an array
# The exact syntax may vary, but it should be unpacked
# Look for the pattern of unpacked arrays, not packed bit-vectors
assert "inner[3]" in content or "logic inner" in content
# Should NOT use packed bit-vector syntax like [2:0]inner
assert "[2:0]inner" not in content

View File

@@ -96,3 +96,10 @@ class TestStructGenerator:
assert "[" in result and "]" in result
# Should reference the register
assert "my_regs" in result
# Should use unpacked array syntax (name[size]), not packed bit-vector ([size:0]name)
assert "my_regs[4]" in result
# Should NOT use packed bit-vector syntax
assert "[3:0]my_regs" not in result
# Should be unpacked struct, not packed
assert "typedef struct {" in result
assert "typedef struct packed" not in result

View File

@@ -41,9 +41,9 @@ def test_external_nested_components_generate_correct_decoder(external_nested_rdl
assert "cpuif_rd_sel.multicast.response" not in content
# Verify struct is flat (no nested structs for external children)
assert "typedef struct packed" in content
assert "typedef struct" in content
assert "logic multicast;" in content
assert "logic [15:0]port;" in content
assert "logic port[16];" in content
def test_external_nested_components_generate_correct_interfaces(external_nested_rdl: AddrmapNode) -> None: