Code Generation Steps

PySilicon ships four built-in build steps that generate the C++ headers and helpers a Vitis HLS kernel needs from Python schema definitions. All four are Buildable subclasses — the legacy code-generation base class — and are imported from their respective modules:

Step Purpose Module
StreamUtilsStep Copy synthesizable + testbench stream helpers into the include directory pysilicon.build.streamutils
MemMgrStep Copy memory-manager headers into the include directory pysilicon.build.streamutils
DataSchemaStep Generate the C++ header pair for one DataSchema class pysilicon.hw.dataschema
ArrayUtilsStep Generate packed-array helpers for one scalar element type pysilicon.hw.arrayutils

Each step is typically added once per design (or once per schema, in the case of DataSchemaStep), and the dependency wiring among them is automatic.


StreamUtilsStep

Copies the stream-helper C++ files into a chosen include directory. Every DataSchemaStep and ArrayUtilsStep depends on these headers, so StreamUtilsStep must be added to the DAG first.

from pysilicon.build.streamutils import StreamUtilsStep

dag.add(StreamUtilsStep(output_dir="include"))
Parameter Description
output_dir Directory relative to config.root_dir where the headers are written. Defaults to ".".

Outputs:

  • <output_dir>/streamutils_hls.h — synthesizable AXI-stream types and serialization primitives.
  • <output_dir>/streamutils_tb.h — testbench file I/O and JSON helpers.
  • <output_dir>/streamutils.cpp — companion implementation file. Written only when config.vitis_version_tuple() < (2025, 1) or config.vitis_version is None. If a stale streamutils.cpp exists from an older build and the current Vitis version is ≥ 2025.1, the step deletes it.

MemMgrStep

Copies the memory-manager helper headers. Use when a design needs the memmgr primitives. Independent of StreamUtilsStep — no auto-wiring between them.

from pysilicon.build.streamutils import MemMgrStep

dag.add(MemMgrStep(output_dir="include"))
Parameter Description
output_dir Directory relative to config.root_dir where the headers are written. Defaults to ".".

Outputs:

  • <output_dir>/memmgr.hpp
  • <output_dir>/memmgr_tb.hpp

DataSchemaStep

Generates a pair of C++ headers for one DataSchema class:

  • <include_dir>/<schema_name>.h — synthesizable struct definition + serialization methods.
  • <include_dir>/<schema_name>_tb.h — testbench file I/O and JSON helpers.
from pysilicon.hw.dataschema import DataSchemaStep

dag.add(DataSchemaStep(
    PolyCmdHdr,
    word_bw_supported=[32, 64],
    include_dir="include",
))
Parameter Description
schema_cls The DataSchema subclass to generate headers for.
word_bw_supported List of word widths (e.g. [32, 64]) to generate serialization methods for.
include_dir Directory relative to config.root_dir where the headers are written.
include_filename Override the default output filename (optional).

Dependency wiring

When added to a BuildDag, DataSchemaStep automatically:

  1. Wires itself to the StreamUtilsStep already in the DAG (required — raises ValueError if none is found).
  2. Wires itself to any DataSchemaStep instances for schema types it depends on (e.g. if PolyCmdHdr contains a CoeffArray field, it wires to the DataSchemaStep for CoeffArray).

The #include paths in the generated headers automatically point to the correct relative locations based on this wiring.

Adding steps in dependency order

Schema dependencies must be added before the schemas that reference them. For the poly example:

dag.add(StreamUtilsStep(output_dir="include"))

# Leaves first (no schema dependencies):
dag.add(DataSchemaStep(PolyErrorField, word_bw_supported=[32, 64], include_dir="include"))
dag.add(DataSchemaStep(CoeffArray,     word_bw_supported=[32, 64], include_dir="include"))

# Containers next:
dag.add(DataSchemaStep(PolyCmdHdr,  word_bw_supported=[32, 64], include_dir="include"))
dag.add(DataSchemaStep(PolyRespHdr, word_bw_supported=[32, 64], include_dir="include"))

If a SCHEMA_CLASSES list is already ordered correctly (leaf types first), the list-comprehension form is concise:

schema_steps = [
    dag.add(DataSchemaStep(cls, word_bw_supported=WORD_BW_SUPPORTED, include_dir="include"))
    for cls in SCHEMA_CLASSES
]

include_dir vs. class-level include_dir

The step-level include_dir takes precedence over any include_dir class attribute on the schema. The recommended pattern is to not set include_dir on the schema class — keep the schema free of build-system concerns and specify the location per-step:

# Preferred: schema class has no include_dir
class PolyCmdHdr(DataList):
    elements = { ... }

# include_dir is specified at the step level
dag.add(DataSchemaStep(PolyCmdHdr, word_bw_supported=[32, 64], include_dir="include"))

ArrayUtilsStep

Generates packed-array helper headers for a scalar element type. The output provides C++ functions for reading and writing arrays of that type across AXI streams and arrays at every supported word width.

from pysilicon.hw.arrayutils import ArrayUtilsStep

dag.add(ArrayUtilsStep(Float32, [32, 64]))
Parameter Description
elem_type A DataSchema subclass for the scalar element type (e.g. Float32, PixelField).
word_bw_supported List of word widths to generate helpers for.

Outputs:

  • <elem_type.include_dir>/<name>_array_utils.h — synthesizable helpers.
  • <elem_type.include_dir>/<name>_array_utils_tb.h — testbench helpers.

The output directory comes from elem_type.include_dir, so the element-type specialization should include include_dir:

Float32 = FloatField.specialize(bitwidth=32, include_dir="include")
dag.add(ArrayUtilsStep(Float32, [32, 64]))
# writes to include/float32_array_utils.h

ArrayUtilsStep automatically wires itself to the StreamUtilsStep in the DAG.


A note on Buildable and rebuild semantics

All four steps above subclass Buildable, which predates the consumes / produces model used by newer BuildStep subclasses. Practical implications for users:

  • The DAG cannot mtime-skip Buildable steps — they re-run on every dag.run(). This is normally fine because writing four small text files is cheap and deterministic, and downstream Vitis steps are skipped on freshness only if these outputs land unchanged. If you need finer control, force a single step with force=["StreamUtilsStep"] to trigger downstream cascade.
  • Buildable steps wire their dependencies via a resolve_deps() hook rather than declared consumes lists. DataSchemaStep and ArrayUtilsStep use this to find the StreamUtilsStep in the DAG; you don’t need to set anything up.

New code-generation steps you write should subclass BuildStep directly with explicit consumes / produces — see Core Components for the modern pattern.


Complete example

The full codegen sub-DAG for the poly accelerator:

from pysilicon.build.build import BuildConfig, BuildDag
from pysilicon.build.streamutils import StreamUtilsStep
from pysilicon.hw.arrayutils import ArrayUtilsStep
from pysilicon.hw.dataschema import DataSchemaStep

def gen_vitis_code(example_dir, include_dir="include"):
    config = BuildConfig(root_dir=example_dir)
    dag = BuildDag()

    dag.add(StreamUtilsStep(output_dir=include_dir))

    schema_steps = [
        dag.add(DataSchemaStep(cls, word_bw_supported=[32, 64], include_dir=include_dir))
        for cls in [PolyErrorField, CoeffArray, PolyCmdHdr, PolyRespHdr]
    ]

    dag.add(ArrayUtilsStep(Float32, [32, 64]))

    results = dag.run(config)
    return [results[step.name].artifacts["include"] for step in schema_steps]

In a larger build that also runs Vitis, this would typically be wrapped in a single GenCppStep(BuildStep) so the codegen sub-DAG is one node in the outer pipeline — see the Python Simulation Pattern and Vitis Pattern pages for how that wrapping is done in practice.


This site uses Just the Docs, a documentation theme for Jekyll.