Skip to content

Commit 6b78c3a

Browse files
authored
[feat] Support ExternalSV parameter overrides
Adds support for SystemVerilog parameter overrides on ExternalSV descriptors and propagates those overrides through both Verilator compilation and PyCDE wrapper generation.
2 parents c1c42ac + 0fff71e commit 6b78c3a

File tree

11 files changed

+247
-12
lines changed

11 files changed

+247
-12
lines changed

docs/design/external/ExternalSV_zh.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- `python/assassyn/ir/module/external.py` 解析注解构造 `_ExternalConfig`,并通过元类直接构造 `ExternalIntrinsic`,兼容属性访问与 `[]` 操作。
1010
- `Singleton.peek_builder()` 提供的构建器上下文确保在缺失 `with SysBuilder` 的情况下也能生成合法的 `ExternalIntrinsic`/`PureIntrinsic` 节点,并延迟应用构造阶段的输入默认值 (`_apply_pending_connections`)。
1111
- `ExternalIntrinsic``PureIntrinsic.EXTERNAL_OUTPUT_READ` 定义在 `python/assassyn/ir/expr/intrinsic.py`,由后端统一调度。
12+
- `ExternalSV` 支持通过 `__parameters__` 提供 SystemVerilog 参数覆盖(`int/bool/str``PathLike`),供 Verilator 编译与 PyCDE wrapper 统一使用。
1213

1314
## Verilog 生成
1415
- `python/assassyn/codegen/verilog/design.py:_generate_external_module_wrapper` 基于 ExternalSV 声明生成 PyCDE wrapper。

python/assassyn/codegen/simulator/verilator.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Dataclass capturing the direction, type information, and host language types for
4949

5050
### `ExternalFFIModule`
5151

52-
Dataclass that tracks all information for a generated crate: crate name, paths, symbol prefix, IO port descriptors, clock/reset flags, and the produced shared library metadata.
52+
Dataclass that tracks all information for a generated crate: crate name, paths, symbol prefix, IO port descriptors, clock/reset flags, parameter overrides, and the produced shared library metadata.
5353

5454
These types appear in `__all__`, making them available to other generator components.
5555

@@ -72,7 +72,8 @@ Creates an `ExternalFFIModule` spec from an `ExternalSV` **class** (rather than
7272
3. Allocates unique crate and dynamic library names
7373
4. Copies the SystemVerilog source to the crate's `rtl/` directory
7474
5. Calls `_collect_ports_from_class` to partition ports into inputs and outputs
75-
6. Returns a fully populated `ExternalFFIModule` with the class name as `original_module_name`
75+
6. Copies `metadata["parameters"]` (if any) into the spec so Verilator can apply `-G` overrides
76+
7. Returns a fully populated `ExternalFFIModule` with the class name as `original_module_name`
7677

7778
### `_collect_ports` / `_collect_ports_from_class` / `_dtype_to_port`
7879

@@ -97,14 +98,14 @@ Writes `Cargo.toml`, `src/lib.rs`, and `src/wrapper.cpp` for a given spec before
9798

9899
Runs the full native toolchain:
99100
1. Ensures the `.sv` file is present (`_ensure_sv_source`).
100-
2. Calls Verilator (`_run_verilator_compile`) into `build/verilated`.
101+
2. Calls Verilator (`_run_verilator_compile`) into `build/verilated`, passing any `ExternalSV` parameter overrides via `-G`.
101102
3. Collects all generated C++ sources (`_gather_source_files`).
102103
4. Builds the shared library via `_build_compile_command` and `_run_subprocess`.
103104
5. Writes `.verilator-lib-path` so the Rust wrapper knows where to load the artifact.
104105

105106
### `_write_manifest_file`
106107

107-
Takes a manifest path plus a list of specs and rewrites the JSON summary in a single helper. This avoids duplicating the `json.dumps(..., indent=2)` call across the different generation entry points.
108+
Takes a manifest path plus a list of specs and rewrites the JSON summary (including `parameters`) in a single helper. This avoids duplicating the `json.dumps(..., indent=2)` call across the different generation entry points.
108109

109110
### `_record_used_name_hints`
110111

python/assassyn/codegen/simulator/verilator.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class ExternalFFIModule: # pylint: disable=too-many-instance-attributes
5151
sv_rel_path: str
5252
inputs: List[FFIPort] = field(default_factory=list)
5353
outputs: List[FFIPort] = field(default_factory=list)
54+
parameters: Dict[str, object] = field(default_factory=dict)
5455
has_clock: bool = False
5556
has_reset: bool = False
5657
original_module_name: str = ""
@@ -190,9 +191,29 @@ def _run_verilator_compile(crate: ExternalFFIModule, sv_source: Path, obj_dir: P
190191
"--Mdir",
191192
str(obj_dir),
192193
]
194+
if crate.parameters:
195+
for name in sorted(crate.parameters):
196+
verilator_cmd.append(_format_verilator_parameter(name, crate.parameters[name]))
193197
_run_subprocess(verilator_cmd)
194198

195199

200+
def _format_verilator_parameter(name: str, value: object) -> str:
201+
"""Format a SystemVerilog parameter override for Verilator's -G flag."""
202+
if isinstance(value, bool):
203+
literal = "1" if value else "0"
204+
elif isinstance(value, int):
205+
literal = str(value)
206+
elif hasattr(value, "__fspath__"):
207+
literal = json.dumps(os.fspath(value))
208+
elif isinstance(value, str):
209+
literal = json.dumps(value)
210+
else:
211+
raise TypeError(
212+
f"Unsupported ExternalSV parameter type for {name}: {type(value)}"
213+
)
214+
return f"-G{name}={literal}"
215+
216+
196217
def _resolve_verilator_paths() -> tuple[Path, Path]:
197218
"""Locate the Verilator include directories."""
198219
verilator_root = os.environ.get("VERILATOR_ROOT")
@@ -348,6 +369,7 @@ def _create_external_spec(
348369
sv_rel_path=os.path.join("rtl", src_sv_path.name),
349370
inputs=ports_in,
350371
outputs=ports_out,
372+
parameters={},
351373
has_clock=getattr(module, "has_clock", False),
352374
has_reset=getattr(module, "has_reset", False),
353375
original_module_name=module.name,
@@ -364,19 +386,17 @@ def _create_external_spec_from_class(
364386
metadata = external_class.metadata()
365387
top_module = metadata.get("module_name")
366388
if not top_module:
367-
msg = (
389+
raise ValueError(
368390
f"ExternalSV class {external_class.__name__} "
369391
"must specify '__module_name__'"
370392
)
371-
raise ValueError(msg)
372393

373394
file_path = metadata.get("source")
374395
if not file_path:
375-
msg = (
396+
raise ValueError(
376397
f"ExternalSV class {external_class.__name__} "
377398
"must specify '__source__'"
378399
)
379-
raise ValueError(msg)
380400

381401
crate_name = _unique_name(
382402
f"verilated_{_sanitize_base_name(top_module, external_class.__name__)}",
@@ -398,6 +418,7 @@ def _create_external_spec_from_class(
398418
shutil.copy(src_sv_path, crate_path / "rtl" / src_sv_path.name)
399419

400420
ports_in, ports_out = _collect_ports_from_class(external_class)
421+
parameters = metadata.get("parameters", {}) or {}
401422

402423
return ExternalFFIModule(
403424
crate_name=crate_name,
@@ -409,6 +430,7 @@ def _create_external_spec_from_class(
409430
sv_rel_path=os.path.join("rtl", src_sv_path.name),
410431
inputs=ports_in,
411432
outputs=ports_out,
433+
parameters=dict(parameters),
412434
has_clock=metadata.get("has_clock", False),
413435
has_reset=metadata.get("has_reset", False),
414436
original_module_name=external_class.__name__,
@@ -428,6 +450,7 @@ def _spec_manifest_entry(spec: ExternalFFIModule, simulator_root: Path) -> Dict[
428450
"has_reset": spec.has_reset,
429451
"lib_filename": spec.lib_filename,
430452
"lib_path": str(spec.lib_path) if spec.lib_path else "",
453+
"parameters": dict(spec.parameters),
431454
"inputs": [
432455
{
433456
"name": port.name,

python/assassyn/codegen/verilog/_expr/intrinsics.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ This function generates Verilog code for block intrinsic operations, which are c
107107
- The cleanup phase incorporates these stored predicates into post-wait assignments and triggers via `get_pred`
108108

109109
4. **EXTERNAL_INSTANTIATE / ExternalIntrinsic**: Creates and wires external modules in-line
110-
- `ExternalIntrinsic` instances are handled before the opcode switch, generating calls to `<wrapper>::new()` and wiring all inputs
110+
- `ExternalIntrinsic` instances are handled before the opcode switch, generating calls to `@modparams` wrappers and wiring all inputs
111+
- Parameter overrides from `ExternalSV.metadata()["parameters"]` are formatted and passed into the wrapper factory before port connections
111112
- Updates the dumper's bookkeeping (`external_instance_names`, `external_wrapper_names`, `external_output_exposures`) while consulting the shared `ExternalRegistry` for instance owners and cross-module consumers
112113

113114
The function integrates with the credit-based pipeline architecture by managing execution conditions and finish signals.

python/assassyn/codegen/verilog/_expr/intrinsics.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
including PureIntrinsic, Intrinsic, and Log.
66
"""
77

8+
import os
89
from typing import Optional, TYPE_CHECKING
910
from string import Formatter
1011

@@ -202,6 +203,19 @@ def _handle_external_output(dumper, expr, intrinsic, rval):
202203
return result
203204

204205

206+
def _format_external_param_value(name: str, value: object) -> str:
207+
"""Format ExternalSV parameter values for PyCDE wrapper instantiation."""
208+
if isinstance(value, bool):
209+
return "1" if value else "0"
210+
if isinstance(value, int):
211+
return str(value)
212+
if hasattr(value, "__fspath__"):
213+
return repr(os.fspath(value))
214+
if isinstance(value, str):
215+
return repr(value)
216+
raise TypeError(f"Unsupported ExternalSV parameter type for {name}: {type(value)}")
217+
218+
205219
def codegen_pure_intrinsic(dumper, expr: PureIntrinsic) -> Optional[str]:
206220
"""Generate code for pure intrinsic operations."""
207221
intrinsic = expr.opcode
@@ -233,6 +247,14 @@ def codegen_external_intrinsic(dumper, expr: ExternalIntrinsic) -> Optional[str]
233247
wrapper_name = f"{ext_class.__name__}_ffi"
234248
dumper.external_wrapper_names[ext_class] = wrapper_name
235249

250+
parameters = metadata.get("parameters", {}) or {}
251+
param_args = []
252+
if parameters:
253+
for name in sorted(parameters):
254+
param_args.append(
255+
f"{name}={_format_external_param_value(name, parameters[name])}"
256+
)
257+
236258
connections = []
237259
if metadata.get('has_clock'):
238260
connections.append('clk=self.clk')
@@ -242,7 +264,14 @@ def codegen_external_intrinsic(dumper, expr: ExternalIntrinsic) -> Optional[str]
242264
value_code = dumper.dump_rval(value, False)
243265
connections.append(f"{port_name}={value_code}")
244266

245-
call = f"{wrapper_name}({', '.join(connections)})" if connections else f"{wrapper_name}()"
267+
wrapper_factory = (
268+
f"{wrapper_name}({', '.join(param_args)})" if param_args else f"{wrapper_name}()"
269+
)
270+
call = (
271+
f"{wrapper_factory}({', '.join(connections)})"
272+
if connections
273+
else f"{wrapper_factory}()"
274+
)
246275
dumper.external_instance_names[expr] = rval
247276

248277
entries = dumper.external_metadata.reads_for_instance(expr)

python/assassyn/codegen/verilog/design.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ The CIRCTDumper class is the main visitor that converts Assassyn IR into Verilog
112112
1. **Execution Control**: `wait_until` and per-expression `meta_cond` metadata decide when statements run, while FINISH gating now reads the precomputed `finish_sites` stored in module metadata instead of collecting tuples during emission.
113113
2. **Module State**: `current_module` tracks traversal context, while port declarations are derived from immutable metadata instead of mutating dumper dictionaries.
114114
3. **Array Management**: `array_metadata`, `memory_defs`, and ownership metadata ensure multi-port register arrays are emitted while memory payloads (`array.is_payload(memory)` returning `True`) are routed through dedicated generators.
115-
4. **External Integration**: `external_metadata` (an `ExternalRegistry`) captures external classes, instance ownership, and cross-module reads. Runtime maps (`external_wrapper_names`, `external_instance_names`, `external_wire_assignments`, `external_wire_outputs`, and `external_output_exposures`) reuse that registry to materialise expose/valid ports and wire consumers to producers without recomputing analysis.
115+
4. **External Integration**: `external_metadata` (an `ExternalRegistry`) captures external classes, instance ownership, and cross-module reads. Runtime maps (`external_wrapper_names`, `external_instance_names`, `external_wire_assignments`, `external_wire_outputs`, and `external_output_exposures`) reuse that registry to materialise expose/valid ports and wire consumers to producers without recomputing analysis. `_generate_external_module_wrapper` now emits `@modparams` wrappers and threads `ExternalSV.metadata()["parameters"]` into instantiations so SystemVerilog parameter overrides propagate into PyCDE.
116116
5. **Expression Naming**: `expr_to_name` and `name_counters` guarantee deterministic signal names whenever expression results must be reused across statements.
117117
6. **Code Generation**: `code`, `logs`, and `indent` store emitted lines and diagnostic information used later by the testbench.
118118
7. **Module Metadata**: `module_metadata` maps each `Module` to its `ModuleMetadata`. The structure tracks FINISH intrinsics, async calls, FIFO interactions (annotated with `expr.meta_cond`), and every array/value exposure required for cleanup. These entries are populated before the dumper is constructed via [`collect_fifo_metadata`](./analysis.md), so `CIRCTDumper` receives a frozen snapshot and never mutates it during emission. See [metadata module](/python/assassyn/codegen/verilog/metadata.md) for details. The dumper exposes this information via convenience helpers such as `async_callers(module)`, which forwards to the frozen `AsyncLedger` stored on the interaction matrix.

python/assassyn/codegen/verilog/design.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,10 +294,19 @@ def _generate_external_module_wrapper(self, ext_class):
294294
class_name = f"{ext_class.__name__}_ffi"
295295
metadata = ext_class.metadata()
296296
module_name = metadata.get('module_name', ext_class.__name__)
297+
parameters = metadata.get("parameters", {}) or {}
298+
param_names = sorted(parameters)
297299

298300
self.external_wrapper_names[ext_class] = class_name
299301

300-
self.append_code(f'class {class_name}(Module):')
302+
self.append_code('@modparams')
303+
if param_names:
304+
self.append_code(f'def {class_name}({", ".join(param_names)}):')
305+
else:
306+
self.append_code(f'def {class_name}():')
307+
self.indent += 4
308+
impl_name = f"{class_name}Impl"
309+
self.append_code(f'class {impl_name}(Module):')
301310
self.indent += 4
302311

303312
# Set the module name for PyCDE
@@ -315,6 +324,8 @@ def _generate_external_module_wrapper(self, ext_class):
315324
else:
316325
self.append_code(f'{wire_name} = Output({wire_type})')
317326

327+
self.indent -= 4
328+
self.append_code(f'return {impl_name}')
318329
self.indent -= 4
319330
self.append_code('')
320331

python/assassyn/ir/module/external.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class MyIP(ExternalSV):
2020
y: WireOut[UInt(32)]
2121
__source__ = "rtl/my_ip.sv"
2222
__module_name__ = "my_ip"
23+
__parameters__ = {"ADDR_WIDTH": 8, "INIT_FILE": "rtl/init.hex"}
2324

2425
inst = MyIP(a=value_a, b=value_b) # returns ExternalIntrinsic
2526
result = inst.y # wire output → PureIntrinsic(EXTERNAL_OUTPUT_READ)
@@ -38,6 +39,7 @@ result = inst.y # wire output → PureIntrinsic(EXTERNAL_OUTPUT_READ)
3839

3940
* The decorator validates that the class extends `ExternalSV`, walks `__annotations__`, and gathers all `WireIn`/`WireOut`/`RegOut` definitions into the `_wires` metadata table.
4041
* Configuration fields such as `__source__`, `__module_name__`, `__has_clock__`, and `__has_reset__` are captured so code generation stages can decide how to wrap and clock the external block.
42+
* `__parameters__` (optional dict) captures SystemVerilog parameter overrides; values may be `int`/`bool`/`str` (or `PathLike`, converted to `str`, with `bool` coerced to `int`) and are surfaced to both Verilator (`-G`) and PyCDE wrapper generation.
4143
* The decorated class remains callable; invoking it runs through the metaclass and returns an `ExternalIntrinsic` instead of a Python object. There is no longer a mutable Python instance that exposes setters/getters.
4244

4345
-----
@@ -46,6 +48,8 @@ result = inst.y # wire output → PureIntrinsic(EXTERNAL_OUTPUT_READ)
4648

4749
* **Construction**: The metaclass intercepts calls to the class and routes them to `_create_external_intrinsic`, which wraps the request in an `ExternalIntrinsic`.
4850
* **Metadata**: `_wires` stores port declarations (direction + kind + dtype) while `_metadata` records auxiliary fields such as `module_name`, `source`, clock/reset booleans, etc. Downstream code generation stages read these tables directly.
51+
* **Parameters**: `__parameters__` is folded into `_metadata["parameters"]` so simulator + Verilog backends can apply SV parameter overrides consistently.
52+
* **Parameter Access**: `ExternalSV.parameters()` returns a copy of the normalized parameter map.
4953
* **No Mutable Instance**: The descriptor no longer inherits from `Downstream` or exposes mutation APIs like `in_assign`. All connectivity is described by the returned intrinsic and its operands.
5054
* **Debugging Support**: `__repr__` is implemented on the Python side for better logging, but day-to-day interaction happens through the intrinsic nodes.
5155

python/assassyn/ir/module/external.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,48 @@
55
# pylint: disable=duplicate-code,too-few-public-methods
66

77
from dataclasses import dataclass
8+
from os import fspath
89
from typing import Any, Dict, Generic, TypeVar, Literal
910

1011

1112
T = TypeVar('T')
1213

1314

15+
def _normalize_external_parameters(parameters: Any) -> Dict[str, Any]:
16+
"""Normalize ExternalSV parameter overrides into a plain dict.
17+
18+
Accepted parameter values:
19+
- int/bool (bool coerced to int)
20+
- str
21+
- PathLike (converted via os.fspath)
22+
"""
23+
if parameters is None:
24+
return {}
25+
if not isinstance(parameters, dict):
26+
raise TypeError("ExternalSV __parameters__ must be a dict of name -> value")
27+
28+
normalized: Dict[str, Any] = {}
29+
for name, value in parameters.items():
30+
if not isinstance(name, str):
31+
raise TypeError("ExternalSV parameter names must be strings")
32+
if isinstance(value, bool):
33+
normalized[name] = int(value)
34+
continue
35+
if isinstance(value, int):
36+
normalized[name] = value
37+
continue
38+
if hasattr(value, "__fspath__"):
39+
normalized[name] = fspath(value)
40+
continue
41+
if isinstance(value, str):
42+
normalized[name] = value
43+
continue
44+
raise TypeError(
45+
"ExternalSV parameter values must be int/bool/str or PathLike"
46+
)
47+
return normalized
48+
49+
1450
def _create_external_intrinsic(cls, **input_connections):
1551
"""Factory function to create ExternalIntrinsic with proper IR builder tracking."""
1652
# pylint: disable=import-outside-toplevel
@@ -160,6 +196,7 @@ class MyExternal(ExternalSV):
160196
'module_name': getattr(cls, '__module_name__', cls.__name__),
161197
'has_clock': getattr(cls, '__has_clock__', False),
162198
'has_reset': getattr(cls, '__has_reset__', False),
199+
'parameters': _normalize_external_parameters(getattr(cls, '__parameters__', None)),
163200
})
164201

165202
return cls
@@ -206,3 +243,9 @@ def port_specs(cls) -> Dict[str, WireSpec]:
206243
def metadata(cls) -> Dict[str, Any]:
207244
"""Return metadata dictionary for the external module."""
208245
return cls._metadata
246+
247+
@classmethod
248+
def parameters(cls) -> Dict[str, Any]:
249+
"""Return a copy of the normalized external parameter overrides."""
250+
params = cls._metadata.get("parameters", {})
251+
return dict(params)

0 commit comments

Comments
 (0)