Skip to main content

Tasks and Functions Enhancements

Verilog's Functions and Tasks page covered the core rules — functions return one value in zero time, tasks can consume time and have multiple outputs, and automatic should always be written explicitly since Verilog defaults to (bug-prone) static locals. SystemVerilog keeps every one of those rules and adds a handful of call-site conveniences that make functions/tasks genuinely pleasant to use in testbench code, where they're called constantly.

automatic by default​

The one behavioral default SystemVerilog actually changes: functions and tasks declared inside a class (covered starting in Section C) or a package are automatic by default, removing the need to write it explicitly in those contexts. Free-standing module-level functions/tasks still default to static, exactly as in plain Verilog — the explicit automatic habit from Functions and Tasks is still the right one to keep for those.

Default argument values​

function automatic void report(string msg, string severity = "INFO");
$display("[%s] %s", severity, msg);
endfunction

report("starting test"); // severity defaults to "INFO"
report("mismatch detected", "ERROR"); // explicit override

A parameter with = value in its declaration can be omitted at the call site, falling back to that default — exactly the same idea as default arguments in most software languages, with no equivalent in plain Verilog, where every call had to supply every argument every time.

void functions​

function automatic void report(string msg);
$display("%s", msg);
endfunction

A void function returns nothing and is called as its own statement, not inside an expression — something plain Verilog's function rules don't allow (every plain Verilog function must return exactly one value). This closes a real gap: a function-like piece of reusable code that only has side effects (printing, updating a queue, incrementing a counter) previously had to be written as a task, even when it needed none of a task's time-consuming or multi-output capability.

ref and const ref arguments: pass by reference​

function automatic void scale_all(ref int data [], input int factor);
foreach (data[i])
data[i] *= factor;
endfunction

int values [0:3] = '{1, 2, 3, 4};
scale_all(values, 10); // values is now {10, 20, 30, 40} — modified in place

Plain Verilog only passes arguments by value — a task/function argument is a copy, and modifying it inside the function never affects the caller's variable. ref passes by reference instead: the function operates directly on the caller's variable, and changes are visible immediately after the call returns — essential for a function meant to modify a large array or queue in place, since copying it in and back out by value would be both slower and, for genuinely shared state, wrong. const ref gets the same by-reference efficiency (no copy) while still preventing the function from modifying the argument — the right choice for a large read-only argument (e.g., a queue a function only needs to inspect).

A ref argument can only be used in a function/task that is itself declared automatic — a static subroutine's reference argument would be unsafe to alias against a caller's variable across separate calls, so the language simply disallows it, one more reason scale_all above is written function automatic void scale_all(...) rather than leaving automatic off.

Recursion needs automatic​

function automatic int factorial(int n);
if (n <= 1) return 1;
return n * factorial(n - 1);
endfunction

A function or task calling itself needs its own separate copy of its local variables (and arguments) for each nested call in progress — exactly what automatic provides, and exactly what a static lifetime cannot: with static, every call (including a recursive one calling itself) shares one memory location per variable, so a recursive call's changes silently clobber the outer call's still-in-progress state instead of getting its own frame. This is the sharpest version of the same automatic-vs-static distinction Functions and Tasks already introduced — recursion simply cannot work correctly without it, rather than merely being a latent bug risk.

One more default-argument restriction worth knowing: default values are only legal on input, inout, and ref arguments — an output argument can never have one, since its whole purpose is to be written by the callee, not read from a caller-supplied fallback.

return​

function automatic int find_first_negative(int data []);
foreach (data[i]) begin
if (data[i] < 0)
return i; // exits immediately with this value
end
return -1; // not found
endfunction

Plain Verilog has no return statement — a function's result is set by assigning to the function's own name, and the function always runs to its final end regardless. SystemVerilog's return exits immediately from anywhere in the function body, which is what makes the early-exit pattern above (stop as soon as a match is found) possible without extra flag variables to fake an early exit.

What's next​

Functions and tasks are single reusable procedures. The next page covers interface — a way to bundle an entire group of related signals (and, optionally, the tasks/functions that operate on them) into one reusable connection point, replacing the long port lists that grow unwieldy as a design scales up.