Verilog · Chapter 15.4 · Tasks & Functions
Re-entrant Tasks & Functions in Verilog — Recursion & Concurrency
A task or function is re-entrant if it can be safely entered again while a previous call is still in progress, that is, called recursively when it calls itself or concurrently when the same routine runs from several parallel processes at once. Reentrancy requires that each invocation have its own storage, which is exactly what the automatic keyword provides. This makes two patterns possible. Recursion is where a function calls itself with a smaller problem, such as a factorial or a tree traversal, and each level must keep its own locals. Concurrent calls are where the same task runs from multiple fork threads or always blocks at once, and each thread's call must stay independent. This page explains re-entrancy, the automatic requirement, recursive functions, and concurrent task calls.
Foundation11 min readVerilogReentrantRecursionautomaticConcurrency
Chapter 15 · Section 15.4 · Tasks & Functions
1. The Engineering Problem
A recursive function, or a task called from several places at once, must be re-entrant — safe to enter again while a prior call is still running — which requires per-call storage:
A re-entrant task/function can be safely called recursively or concurrently, which requires
automaticstorage (per-call). A recursive or concurrently-calledstaticroutine corrupts its locals.
This page drills re-entrancy with automatic.
2. Mental Model — Each Active Call Needs Its Own Storage
3. Recursion — A Function Calling Itself
Recursion requires automatic so each level keeps its own locals:
// factorial — recursive, needs automatic:
function automatic integer factorial;
input integer n;
if (n <= 1) factorial = 1;
else factorial = n * factorial(n - 1); // calls itself
endfunction
// usage (elaboration-time constant, e.g. for a parameter):
localparam DEPTH = factorial(4); // = 24Each recursive call of factorial is active while it waits for the deeper call to return, so each needs its own n — which automatic provides. A static factorial would share one n across all levels and compute garbage. Recursion is used for compile-time computations (factorials, logs) and recursive structure generation.
4. Concurrency — The Same Routine in Parallel
Concurrent calls require automatic so the parallel invocations stay independent:
task automatic drive_channel; // automatic → reentrant
input integer ch;
input [7:0] data;
reg [7:0] local_buf; // per-call (independent across channels)
begin
local_buf = data;
@(posedge clk);
bus[ch] = local_buf;
end
endtask
// called concurrently for several channels:
initial fork
drive_channel(0, 8'hA1);
drive_channel(1, 8'hB2);
drive_channel(2, 8'hC3);
joinThe three drive_channel calls run concurrently (from the fork...join), each with its own local_buf because the task is automatic. A static version would share local_buf, and the channels would corrupt each other. Concurrent, reentrant tasks are common in testbenches that drive several interfaces in parallel.
Visual A — reentrancy needs automatic
Reentrancy — recursion and concurrency need automatic
data flow5. Common Mistakes
- Recursion without
automatic— shared static storage corrupts the levels (§3, DebugLab 1). - Concurrent calls without
automatic— parallel invocations clobber (§4, 15.3). - Assuming static is reentrant — it is not; only
automaticis (§2).
6. Debugging Lab
One reentrancy debug post-mortem
Pitfall — recursive function without automatic
// Recursive factorial WITHOUT automatic — static storage shared across levels.
function integer factorial; // static (default)
input integer n;
if (n <= 1) factorial = 1;
else factorial = n * factorial(n - 1); // recursion
endfunction
// Each recursive call is active while waiting for the deeper call to
// return, but the STATIC function shares ONE set of storage (including the
// implicit 'n' handling). The recursion clobbers its own state across
// levels, computing a wrong result (or failing).A recursive function (factorial, tree traversal, etc.) returns a wrong value — the recursion's intermediate state seems corrupted across levels. Some tools error on recursion without automatic.
Recursion requires re-entrancy, which requires automatic storage. A static function has ONE set of storage shared by all calls — but in recursion, multiple calls are active simultaneously (each level is paused waiting for the deeper call to return). With static storage, the deeper call overwrites the outer call's state, corrupting the recursion. Only automatic storage gives each recursive level its own independent locals.
The fix is to declare the function 'automatic', so each recursive call has its own storage and the recursion works correctly.
function automatic integer factorial; // automatic → reentrant/recursive
input integer n;
if (n <= 1) factorial = 1;
else factorial = n * factorial(n - 1);
endfunction
// 'automatic' gives each recursive level its own storage, so factorial(4)
// correctly computes 24. Recursion ALWAYS needs automatic.Pitfall — recursion with no converging base case overflows the stack
// Recursive factorial — automatic (good), but the base case never converges.
function automatic integer factorial;
input integer n;
if (n == 1) factorial = 1; // base case ONLY catches n == 1
else factorial = n * factorial(n - 1);
endfunction
// Looks fine for factorial(4). But consider factorial(0), or any call where
// n starts at 0 or is negative (or a parameter computes to 0):
// factorial(0) → 0 != 1 → factorial(-1) → -1 != 1 → factorial(-2) → ...
// 'n' decreases away from 1 forever. The base case (n == 1) is never reached
// on this path, so the recursion never terminates: each call spawns a deeper
// one, the call stack grows without bound, and the simulator eventually
// crashes with a STACK OVERFLOW (or hangs). 'automatic' fixed the storage —
// it does nothing for a base case that the recursion can step past.A recursive function that works for typical inputs crashes the simulator (stack overflow) or hangs for an edge-case argument — n = 0, a negative value, or a parameter that evaluates to 0. The failure is catastrophic (the whole simulation dies) rather than a wrong value, and it appears only for the unconverging input.
Every recursive path must CONVERGE to a base case, and this one does not. The base case 'n == 1' is only reached when n steps down through exactly 1; for n = 0 or a negative n, the argument decreases away from 1 forever, so the termination condition is never satisfied. Each unconverged call pushes another stack frame (and with 'automatic', a fresh set of locals each time), so the stack grows without bound until the simulator overflows it. The 'automatic' keyword is necessary for correct recursion but is orthogonal to TERMINATION — it gives each level its own storage, not a guarantee that the recursion ends.
The fix is a base case that catches EVERY path that should stop — typically 'n <= 1' rather than 'n == 1' — and validating the argument domain so the recursion cannot be entered with a value it can never converge from.
function automatic integer factorial;
input integer n;
if (n <= 1) factorial = 1; // catches 1, 0, and negatives → converges
else factorial = n * factorial(n - 1);
endfunction
// 'n <= 1' makes every downward path terminate (0 and negatives resolve to 1
// immediately), so the recursion always converges and the stack stays bounded.
//
// SYNTHESIS NOTE: recursion is a simulation/elaboration construct, not
// general synthesizable hardware. A recursive function only maps to hardware
// when its depth is a COMPILE-TIME CONSTANT (a parameter), so the synthesizer
// can fully unroll it into a fixed structure — e.g. an elaboration-time
// computation or a recursively-built parameterized tree (a balanced adder
// tree). A recursion whose depth depends on a RUNTIME value has no fixed
// hardware and is not synthesizable. So: every path must converge (or the sim
// overflows), AND the depth must be statically bounded (or it won't synthesize).7. Interview Q&A
8. Exercises
Exercise 1 — Make it recursive
Write a recursive automatic function that computes 2^n for a constant n.
Exercise 2 — Why automatic?
Explain why a static recursive function corrupts its state but an automatic one does not.
Exercise 3 — Concurrent task
A task is called from three parallel fork threads and gives intertwined results. What declaration fixes it?
9. Summary
Re-entrant tasks and functions are safe to call recursively or concurrently:
- Reentrancy requires
automatic— per-call storage so overlapping calls are independent. - Recursion — a self-calling function needs each level's own storage (
automatic). - Concurrency — the same task in parallel processes needs per-call storage (
automatic). staticis not re-entrant — shared storage corrupts overlapping calls.
The last sub-topic covers advanced patterns: Chapter 15.5 Tasks & Functions Advanced drills synthesis rules, passing arrays, and combining tasks/functions.
Related Tutorials
- Static & Automatic Behaviour — Chapter 15.3; the storage axis reentrancy builds on.
- Function — Chapter 15.1; recursive functions.
- Task — Chapter 15.2; concurrent tasks.
- Tasks & Functions — Chapter 15 overview; where static, automatic, and re-entrant behaviour fit in the chapter.
- Sequential & Parallel Blocks — Chapter 14.2; the
fork...joinconcurrency requiringautomatic.