WYSIWYG Coding Style - WYSIWYG Coding Style - 2025.2 English - UG1399

Vitis High-Level Synthesis User Guide (UG1399)

Document ID
UG1399
Release Date
2026-01-22
Version
2025.2 English

This section defines the canonical dataflow coding style that leads to a dataflow network in one-to-one correspondence with the source code. More advanced features (where the restrictions below are less strict) are described in the next section. Non-canonical dataflow regions are transformed by Vitis HLS into canonical dataflow regions by grouping instructions and control while preserving concurrency among loops and function calls.

The dataflow pragma can be applied in two different contexts:

  • Inside a function (also known as function dataflow region).
  • Inside a special kind of for loop (also known as loop dataflow region) such that:
    • The loop is the only statement inside the body of a function.
    • The loop counter is declared in the loop header and is of int type.
    • The initial value is set to any non-negative numerical integer constant in the loop header.
    • The exit condition is a "<" comparison with a non-negative numerical constant or a scalar argument of the function that encloses the loop.
    • The loop variable is incremented by any positive numerical integer constant.
    • Except the loop counter, variable declarations are inside the loop body.

The WYSIWYG coding style requires that the body of that function or for loop must contain only a sequence of:

  • Local variable declarations without initialization, even those performed automatically by constructors.
    • These variables, also called “channels”, must be initialized in the process that produces them.
    • For standard data types that have default constructors that cannot be redefined (for example: std::complex), one can avoid initialization by using the no_ctor attribute, for example, std::complex<float> arr[SIZE] __attribute__((no_ctor));
  • Sub-function calls
    • Arguments involving type conversions or address computations are not canonical dataflow and may result in additional processes or different network structure.
  • hls::task instantiations

In other words, there cannot be any control inside a “canonical” dataflow region (control structures such as if-then-else and loops would be automatically converted into processes, which results in the need for Vitis HLS to infer the network as discussed above).

These called sub-functions and hls::tasks can be:

  • Sequential functions or pipelined functions, or
  • Function dataflow regions, or
  • Loop dataflow regions.

Example of recommended style for a dataflow function:

void dataflow(int Input0, int Input1[], int &Output0, int Output1[]) {
#pragma HLS dataflow
  int C1[N], C2;  // no initialization
  UserDataType C0 __attribute__((no_ctor)); // no_ctor must be used if the default constructor is not empty
  func1(Input0, Input1, C0, C1); // read Input0, read Input1, write C0, write C1
  func2(C0, C1, C2);             // read C0, read C1, write C2
  func3(C2, Output0, Output1);   // read C2, write Output0, write Output1
}

Example of recommended style for dataflow in loop (note that N can be either a constant value or, as in this case, an input argument to the dataflow region):

void dataflow(int Input0, int Input1[], int &Output0, int Output1[], int N) {
  for (int i = 2; i < N; i += 2) {
    #pragma HLS dataflow
    int C1[N], C2;  // no initialization
    UserDataType C0 __attribute__((no_ctor)); // no_ctor must be used if the default constructor is not empty
    func1(Input0, Input1, C0, C1, i); // read Input0, read Input1, write C0, write C1
    func2(C0, C1, C2, i);             // read C0, read C1, write C2
    func3(C2, Output0, Output1, i);   // read C2, write Output0, write Output1
  }
}
Note: The function where the loop occurs does not require the dataflow pragma, but it must contain only the loop.

Further semantic restrictions about the body of dataflow functions or regions are:

  • Local variables must be non-static scalars or arrays:
    • In canonical dataflow, static variables can be declared within a (non-dataflow) process however. But there should not be multiple instances of this process, otherwise the accesses to the static variable would create a hidden communication channel between these instances;
  • Instances of hls::tasks in a canonical region:
    • Must be declared as hls_thread_local (for example, hls_thread_local hls::task t1(proc, arg1, arg2, arg3); ).
    • Same for instances of hls::stream and hls::stream_of_blocks inside a dataflow region (note that hls_thread_local is similar to static, but it is more appropriate in this context, because it does not imply a shared single variable instance among multiple hls::tasks with the same function body).
  • The processes must transfer data among them using local variables, also called “channels”, either scalars, arrays, or stream-like channels (hls::streams and hls::stream_of_blocks). Unless advanced features and exceptions discussed below:
    • Arrays and stream-like channels must have only one writer process and one reader process, with the writer lexically before the reader:
      • Multiple readers for arrays are possible if the array is marked with #pragma HLS bind_storage variable=... type=ram_1wnr. In this case, the compiler automatically duplicates the variable.
    • Scalar channels can have multiple writer and reader processes (except for top scalars) and are automatically converted into FIFO channels, each with one producer and one consumer. These channels can:
      • Either have explicit, automatically generated, FIFO write and read operations inside the function bodies (these are called “scalar propagation FIFOs”);
      • Or be automatically written by the ap_done of the producer and read by the ap_ready of the consumer (these are called “task level FIFOs”).
    • Channels cannot have loop-carried dependencies:
      • Exceptions are discussed below, in particular feedback stream-like channels are possible so as to generate cyclic dataflow networks.
  • Top kernel arguments can also be passed to processes, but have different restrictions because:
    • No communication among processes can occur using a top kernel argument.
    • For top array arguments mapped to m_axi interfaces, the writer process (if any) must be after the reader (if any), because Vitis HLS assumes that top m_axi arrays do not have carried dependencies, i.e., each execution of the kernel receives a new m_axi mapped buffer in DRAM.