########### 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. .. code-block:: yaml 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. .. code-block:: yaml 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. .. code-block:: yaml 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. .. image:: imgs/task_dataflow.excalidraw.svg 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. .. code-block:: yaml 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. .. code-block:: yaml 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. .. code-block:: yaml 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. .. code-block:: yaml 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: .. code-block:: yaml 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: .. code-block:: yaml 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: .. code-block:: yaml 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. .. code-block:: yaml 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: .. code-block:: yaml 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.