Elaboration
Concept
Code generation needs a module’s structure — its endpoints, its sub-modules, the interfaces wiring them, its boundary. It does not need a running simulation, and it must not depend on one.
elaborate is the single entry that provides exactly that:
from waveflow.build.elaborate import elaborate
from examples.fir_block.fir_block import FirBlock
top = elaborate(FirBlock, {"mem_dwidth": 64, "ntap": 32, "samp_w": 16,
"samp_i": 2, "unroll_lane": True}, name="fir_block")
It constructs the class through its normal __init__ with the given
HwParam / HwConst overrides applied — no immutability bypass, no
special path. What differs from an ordinary instantiation is the context it is built in.
ElabContext — a simulation that never runs
An HwModule is a SimObj and expects a sim=. Elaboration supplies an ElabContext: a subclass of
Simulation that provides exactly what construction needs from the lifecycle — add_obj and a SimPy
env for the resources endpoints allocate — and nothing else. Calling run_sim() on one raises.
The env is created lazily, so a module that declares its structure without touching the sim
allocates nothing at all. That is not a micro-optimization; it is the invariant made visible. Because
ElabContext is a Simulation, construction behaves byte-for-byte as it did back when codegen sites
wrote cls(name="_codegen", sim=Simulation()) by hand — which is what this replaced.
The contract
A module’s structure is a pure function of its
HwParam/HwConstparameters.name,sim, and runtime data are elaboration context and must not affect it.
When that holds, code generation is:
elaborate(class, param-set) → structure → C++
one output per param-set, instance-independent. Any real instance built with those parameters matches the generated C++ by construction — which is the property that lets a build DAG generate code from a class it never simulates, and lets resource attribution read a report back against a module graph rebuilt from the same parameters.
It is also what makes variants well defined: hwgen emits one kernel per
param-set by elaborating the class once per override dict.
DataSchemagets this for free — its structure is class attributes plus classmethods, readable with no instantiation at all.HwModulebuilds structure imperatively in__post_init__, so what a schema has by construction, a module has only by contract. Hence the gate below.
The purity gate
The contract is enforced, not merely documented. On the first elaboration of a given
(class, param-set), assert_param_pure:
- builds the module twice, with different names, in fresh contexts;
- reduces each to a
structure_signature— a canonical token tree that excludes identity, names, the sim, SimPy resources and back-references; - raises
ParamPurityErrorif they differ, naming the first differing attribute path.
ParamPurityError: MyBlock structure is not a pure function of its parameters {'nlane': 4}:
two elaborations produced different structure. Structure (endpoints / sub-components /
interfaces / boundary) must depend only on HwParam/HwConst parameters — not on identity,
global counters, time, randomness, or external mutable state.
First difference at <root>.sub_comps[2].depth
The verdict is cached per (class, param-set), so the gate costs one extra elaboration the first time
and nothing afterwards. Pass check_purity=False to skip it where a caller has already verified.
What it catches is anything that leaks non-parameter state into structure: a module-level counter used
to size a buffer, time, random, or a mutable global read at construction.
What the signature deliberately ignores
| excluded | why |
|---|---|
name, sim, parent, back-references |
identity and context, not structure |
| SimPy objects | per-build identity; only the type tag is kept |
processes, action_history, firing records |
runtime scaffolding |
_resource_model, _timing_model |
see below |
That last row is load-bearing rather than tidy-minded. A module’s calibration key is a digest of this
signature, so if attaching a resource model changed the signature, the key
would move and every store lookup would miss — and add_rm needs the key in order to choose the model
it is attaching. A model is a statement about how hardware was measured, not about what it is.
Where it is used
| caller | what it elaborates for |
|---|---|
hwgen , hwcodegen |
the structure the C++ emitter walks; one kernel per param-set |
codegen_check |
answering check(source, target) without an instance |
InspectSynthStep |
the module graph a synthesis report is attributed against |
module_key |
the identity a calibration record is filed under |
trace_steps |
the two rungs that cost nothing because they are pure elaboration |
Not to be confused with xelab RTL elaboration
Both are called elaboration, and that is not a coincidence — but they run at different stages, on different languages, and produce different things.
Elaboration in EDA generally is the phase after source is parsed and before it is used: the tool walks the design hierarchy from the top down, creates an instance for every instantiation, resolves each instance’s parameter values, binds instance names to their definitions, connects ports, and allocates storage for nets. The output is a fully-resolved instance tree. The parameterized description becomes a concrete design.
xelab is Vivado’s implementation of that for simulation. After xvlog (or xvhdl) compiles RTL
sources into a library, xelab <top> elaborates and links them into a simulation snapshot under
xsim.dir/. Waveflow’s XSI rung runs xelab -dll, which emits xsimk.dll — a
loadable simulator the C++ BFM drives cycle by cycle.
Waveflow elaborate |
xelab |
|
|---|---|---|
| Input | an HwModule class + HwParam values |
compiled Verilog / VHDL |
| Resolves | Python parameters into a module graph | HDL parameters into an instance tree |
| Output | a structural stand-in, read then discarded | a runnable simulation snapshot |
| Stage | before C++ is generated | after C-synthesis has produced RTL |
| Cost | microseconds, no toolchain | seconds to minutes, needs Vivado |
Both happen in a full Waveflow build, in that order: Python elaboration produces the C++ that Vitis
turns into RTL, and xelab then elaborates that RTL so the testbench can drive it. Same idea, two
stages apart.
See also
- Module structure — what “structure” means for codegen: one kernel, endpoints as arguments.
- Parameterization —
HwParamvsHwConst, the inputs elaboration is a pure function of. - Composite kernels — elaboration used to read a synthesis report back against the design that produced it.
- XSI Build Rung — where
xvlog/xelab/ the BFM fit together.