Subroutine contract
Pick a function for a value and a task for an activity.
The distinction is not stylistic: a function must complete without consuming simulation time, while a task can synchronize, delay, and return several results through arguments.
Subroutine selector
Return value, time, and side effects
- 01Typed functionone return value
Zero-time computation that can appear in an expression.
- 02function voidno return value
Zero-time procedure, commonly used for model updates.
- 03taskactivity
May wait, delay, synchronize, and use output/inout arguments.
Zero-time computation that can appear in an expression.
- Task return type
- None
- A task does not have a void return type; function void is different.
- Timing
- Function: zero
- No #, @, wait, or user-defined task call.
- Defaults
- input
- Argument directions should still be written explicitly for review.
Design a subroutine like a small interface: inputs, outputs, time, side effects, and ownership must all be visible.
Reasoning, examples and implementation details
Observe
List outputs, side effects, timing controls, imported calls, and whether the caller uses the result inside an expression.
Decide
Choose a typed function or function void for zero-time work; choose a task when event controls, delays, or activity-style completion are part of the API.
Failure
Hiding time in a function breaks the expression model, while a task used as a pure calculator obscures ownership and makes composition harder.
Prove
Lint timing controls and side effects, unit-test automatic reentrancy, and assert the caller-visible postconditions at the API boundary.
Functions compose; tasks sequence
A non-void function returns through its name or return statement and can participate in an expression. function void performs zero-time side effects without a value. A task represents an operation whose completion is a sequencing event for the caller.
function automatic logic [7:0] parity_mask(
input logic [63:0] data
);
logic [7:0] result;
foreach (result[i]) result[i] = ^data[i*8 +: 8];
return result;
endfunction
task automatic send_beat(
input logic [31:0] data,
output bit accepted
);
req <= 1'b1;
payload <= data;
do @(posedge clk); while (!ready);
accepted = 1'b1;
req <= 1'b0;
endtaskSynthesis depends on behavior, not the keyword alone
A statically bounded, zero-time task can be inlined by synthesis just as a function can. This describes analyzable behavior, not static storage lifetime; an automatic task can also synthesize. Delays, event controls, dynamic testbench objects, and simulator services make a subroutine testbench-only. Review the body and call context rather than declaring every task unsynthesizable.
| Question | Function | Task |
|---|---|---|
| Consumes time? | No | May |
| Used in expression? | Yes, if non-void | No |
| Return values | One typed value plus arguments | Arguments only |
| Can call | Functions only | Functions and tasks |
| Automatic recommended? | Yes for reentrancy | Yes for concurrent calls |
| Synthesizable? | If body is synthesizable | If behavior is statically bounded and zero-time |
Built-ins still carry precise return and mutation rules
System functions and array methods often look self-explanatory, but interview bugs hide in width, four-state treatment, sampling context, and whether the operation mutates its receiver. State those contracts before using the result.
- Array locator methods return queues; choose find, find_index, min, max, unique, or unique_index according to the required result type.
- Reduction and query functions can collapse or expose X/Z differently, so include unknown-state tests at verification boundaries.
- A function may call permitted non-time-consuming system subroutines, but it cannot use a user-defined task as a zero-time escape hatch.
| Operation | Contract | Common review point |
|---|---|---|
| $clog2(n) | Ceiling log2 as an integer | Guard n <= 1 and resulting zero-width declarations |
| $countones(expr) | Count bits that are exactly 1 | X and Z are not counted as ones |
| $onehot(expr) | Exactly one bit is 1 | Use $onehot0 when all-zero is legal |
| $onehot0(expr) | At most one bit is 1 | Unknown control bits still need an explicit policy |
| $cast(dst, src) | Checked runtime conversion | Test success before consuming dst |
| $sampled(expr) | Value sampled for the current assertion attempt | Do not confuse it with an arbitrary procedural read |
| array.shuffle() | Randomize element order in place | The receiver mutates; preserve a copy if order matters later |
| array.find_index with (...) | Queue of matching indices | It does not return matching element values |
Explain it out loud
Interview reasoning checkpoints
A function must complete in zero simulation time and may return a value for an expression. A task may consume time and represents an activity; results travel through output or inout arguments.
- Check for #, @, wait, or time-consuming calls.
- Check whether the caller needs a composable expression value.
- Choose automatic lifetime when calls can overlap.
The expected answer is about time and calling semantics, not just the number of outputs.
Saying tasks are always unsynthesizable or that a task has a void return type.
No. A SystemVerilog function cannot enable a user-defined task, regardless of whether that task happens to contain a timing control. Factor shared zero-time work into another function and keep synchronization in the task caller.
- A function can appear inside an expression whose evaluation must finish in the current region.
- The language restriction is on calling a user-defined task, not only on whether the task body visibly blocks.
- Move synchronization to the task caller and pass computed values through a function.
Look for awareness of transitive timing, not only syntax in the immediate function body.
Assuming an apparently zero-time task is a portable escape hatch.

