Skip to content

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 automatic storage (per-call). A recursive or concurrently-called static routine 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:

recursion.v
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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);     // = 24

Each 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:

concurrency.v
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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);
   join

The 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 flow
Reentrancy — recursion and concurrency need automaticrecursion(self-call)each level activeconcurrency(parallel calls)each call active→ requireautomaticper-call storagecalls independentre-entrant
Both recursion (a function calling itself, each level still active) and concurrency (the same task called from parallel processes) leave multiple calls active at once. Each active call needs its own storage, which automatic provides — making the routine re-entrant. A static routine shares storage and corrupts overlapping calls.

5. Common Mistakes

  1. Recursion without automatic — shared static storage corrupts the levels (§3, DebugLab 1).
  2. Concurrent calls without automatic — parallel invocations clobber (§4, 15.3).
  3. Assuming static is reentrant — it is not; only automatic is (§2).

6. Debugging Lab

One reentrancy debug post-mortem

Pitfall — recursive function without automatic
Buggy Code
// 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).
Symptom

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.

Root Cause

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.

Fix
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
Buggy Code
// 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.
Symptom

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.

Root Cause

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.

Fix
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).
  • static is 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.