Using Tasks¶
Tasks are the fundamental unit of behavior in a DV-Flow specification. Tasks accept data from tasks that they depend on, and produce data to be used by dependent tasks. Tasks may have local parameters to control operation.
The implementation of a task is required to perform three key tasks:
Compute dependencies – Specifically, determine whether the task must perform work. Tasks typically save a ‘memento’ with the system that is used by future evaluations of the task.
Perform work – The task implementation must perform the desired data transformation when required. Usually this involves invoking another tool, such as a compiler. When work is performed, the task may produce markers to communicate the location of errors, warnings, or other key information.
Produce output – Tasks are required to pass data output data to their successor tasks independent of whether the task performed a data transformation.
Most tasks are built on top of another existing task or tasks.These ‘derived’ tasks either modifiy the parameters of the base task to cause it to behave as desired, or compose multiple tasks together to achieve the desired functionality.
Specifying the Base Task¶
Each defined task has a unique identity. Typically, though, the required behavior is very similar to that of an existing task. As a consequence, it is very common to define a task in terms of another. This is done via the uses clause.
tasks:
- name: PrintHello
uses: std.Message
The example above creates a new task named PrintHello that inherits the parameters and implementation of the existing std.Message task.
Specializing Task Parameters¶
The simplest way to leverage existing tasks is to customize the value of parameters that control the behavior of the base task. Let’s take a look at the std.Message task. This task displays a user-specified message string. By default, the message is empty.
tasks:
- name: PrintHello
uses: std.Message
with:
msg: Hello, World!
The example above creates a new task named PrintHello that overrides the value of the msg parameter. This causes the implementation of std.Message to print ‘Hello, World!’.
Specifying Dependencies¶
Most tasks operate on data produced by other tasks. The needs clause specifies the tasks that must complete before this task can execute.
tasks:
- name: rtlfiles
uses: std.FileSet
with:
type: "verilogSource"
base: "rtl"
include: "*.v"
- name: sim
uses: hdlsim.vlt.SimImage
needs: [rtlfiles]
with:
top: [mytop]
- name: run
uses: hdlsim.vlt.SimRun
needs: [sim]
The example above shows three tasks that:
Gather verilog source files from the rtl directory
Compile the files into a simulation image
Run the simulation image
Data passes between each pair of steps above:
rtlfiles outputs a list of sources files required to create a simulation image
The sim-image task passes a path to the simulation image to the task that runs it
In addition, one step cannot proceed until the proceeding step has completed. These scheduling and dataflow requirements are captured using needs relationships.
Tasks and Dataflow¶
Task needs relationships specify dataflow between tasks in addition to scheduling dependencies. A task provides two controls over what input data is provided to the task implementation (if present) and what inputs are forwarded to the output.
The two controls are:
consumes - Specifies what input data will be passed to the implementation
all - All input data is passed to the implementation
none - No input data is passed to the implementation
pattern - Inputs matching a pattern are passed to the implementation
passthrough - Specifies what inputs are passed to the task output
all - All input data is passed to the output
none - No input data is passed to the output
unused - Inputs that are not consumed are passed to the output
pattern - Inputs matching a pattern are passed to the output
The default value of passthrough is always unused. The default value for the consumes` parameter depends on the task type:
Shell or Python Implementation: all
DataItem or No Implementation: none
Using DataItem Tasks¶
Tasks generally produce output data. That output data may be produced by the task’s programming-language implementation (ie Python) when computation of some form is required. When no computation is required, there is concise way to produce a data item: define a task that uses a data type as the base. This is most commonly done when producing a data item that captures a set of tool options.
tasks:
- name: SimOptionsTrace
uses: hdlsim.SimElabArgs
with:
args: [--trace-fst]
- name: sim_img
uses: hdlsim.vlt.SimImage
needs: [SimOptionsTrace]
The SimOptionsTrace task above will produce a single hdlsim.SimElabArgs data item that instructs the Verilator simulator to enable dumping a FST-format waveform file.
This has the same effect as adding –trace-fst directly on the sim_img task, but provides more flexibility:
Tasks can be defined with commonly-used sets of options
Conditional task execution can be used to select between option sets
Using Compound Tasks¶
Compound tasks allow a task to be defined in terms of a set of subtasks. In many cases, this allows more-advanced behavior to be created without the need to provide a programming-language implementation for the task.
tasks:
- name: CreateSVFiles
rundir: inherit
body:
- name: mod1
uses: std.CreateFile
with:
filename: mod1.sv
content: |
module mod1;
endmodule
- name: mod2
uses: std.CreateFile
with:
filename: mod2.sv
content: |
module mod2;
endmodule
- name: getFiles
uses: std.FileSet
passthrough: none
needs: [mod1, mod2]
with:
type: "verilogSource"
base: "."
include: "*.sv"
The compound task above uses the std.CreateFile task to create two SystmeVerilog files. We then want to pass both files on to all tasks that depend on CreateSVFiles.
This is accomplished by:
- Specifying that all tasks use the same run directory via the
rundir: inherit clause.
Causing the getFiles to depend on the file-creation tasks
Conditional Tasks¶
By default, all tasks on the needs dependency path from the root task(s) will be executed. In some cases, though, it is desirable to only execute tasks under specific circumstances. The iff property of tasks supports this use model.
package:
name: my_ip
with:
debug_level:
type: int
value: 0
tasks:
- name: SimOptions
uses: hdlsim.SimElabArgs
body:
- name: SimOptionsDebug
uses: hdlsim.SimElabArgs
iff: ${{ debug_level > 0 }}
with:
args: [--trace-fst]
- name: sim_img
uses: hdlsim.vlt.SimImage
needs: [SimOptions]
The example above uses conditional execution to customize elaboration options. When the debug_level parameter’s value is greater than 0, the SimOptionsDebug task sends the –trace-fst option to the simulator. Otherwise, no additional arguments are provided.
Task Override¶
Tasks can override other tasks using the override field. This allows you to
replace the implementation or parameters of a task defined elsewhere in the flow,
typically to customize behavior for a specific configuration or environment.
package:
name: my_project
tasks:
- name: sim
uses: hdlsim.vlt.SimImage
with:
top: [my_top]
# Override the sim task for debug builds
- name: sim_debug
override: sim
with:
top: [my_top]
debug: true
When a task is overridden, the overriding task replaces the original task
throughout the flow. Any task that depends on sim will now receive the
implementation from sim_debug instead.
Task override is particularly useful for:
Configuration-specific customization: Different build modes (debug, release, etc.)
Tool-specific variants: Swapping between different tool implementations
Testing and debugging: Temporarily replacing a task without modifying the original
Note that task override operates at the package level - an override only affects references within the package where it’s defined, not across package boundaries.
Dataflow Control: Consumes and Passthrough¶
Tasks have fine-grained control over dataflow using the consumes and
passthrough parameters. These control what input data reaches the task
implementation and what inputs are forwarded to the output.
Consumes Patterns¶
The consumes parameter can specify patterns to selectively consume inputs:
tasks:
- name: compile
uses: hdlsim.vlt.SimImage
consumes:
- type: std.FileSet
filetype: systemVerilogSource
This task will only consume FileSet inputs with filetype equal to
systemVerilogSource. Other inputs will be passed through to the output.
The consumes parameter accepts:
all: Consume all inputs (default for tasks with implementations)
none: Consume no inputs (default for DataItem tasks)
Pattern list: List of dictionaries specifying matching criteria
Passthrough Patterns¶
The passthrough parameter controls which inputs are forwarded to output:
tasks:
- name: process
uses: my_tool.Processor
passthrough:
- type: std.FileSet
filetype: verilogSource
This forwards only Verilog source filesets to the output, filtering out other input types.
The passthrough parameter accepts:
all: Pass all inputs to output
none: Pass no inputs to output
unused: Pass only inputs not consumed by the task (default)
Pattern list: List of dictionaries specifying matching criteria
Pattern Matching¶
Patterns match data items by their fields. Multiple fields create an AND condition - all must match:
consumes:
- type: std.FileSet
filetype: systemVerilogSource
attributes: [uvm]
This matches FileSet items that are SystemVerilog sources AND have the ‘uvm’ attribute.
Run Directory Modes¶
Tasks can control how run directories are created using the rundir parameter.
This affects where the task executes and how it organizes its outputs.
tasks:
- name: parent_task
rundir: unique
body:
- name: child_task
rundir: inherit
Run directory modes:
unique: Create a dedicated directory for this task (default)
Each task gets its own isolated workspace
Outputs are cleanly separated
Best for tasks that produce files
inherit: Use the parent task’s directory
Shares workspace with parent
Useful for organizing sub-tasks
Required when sub-tasks need to access each other’s files
The inherit mode is particularly useful for compound tasks that create
multiple files which need to be processed together:
tasks:
- name: CreateTestFiles
rundir: inherit # Share directory across all subtasks
body:
- name: create_file1
uses: std.CreateFile
with:
filename: test1.sv
content: "module test1; endmodule"
- name: create_file2
uses: std.CreateFile
with:
filename: test2.sv
content: "module test2; endmodule"
- name: gather
uses: std.FileSet
needs: [create_file1, create_file2]
with:
include: "*.sv"
All three subtasks share the same directory, so the gather task can
find both created files.