A delay pipeline is written for STAGES = 2. Later, a block that needs no delay reuses it with STAGES = 0. Nothing in the module rejects that value, so the storage range becomes [0:-1], which is two entries, not none. The output reads entry -1, which nothing ever writes.
// snippet: the unguarded body, instantiated with STAGES = 0
T stage_q [0:STAGES-1]; // [0:-1]: two entries
always_ff @(posedge clk) begin
stage_q[0] <= data_i;
for (int i = 1; i < STAGES; i++) stage_q[i] <= stage_q[i-1];
end
assign data_o = stage_q[STAGES-1]; // stage_q[-1]: never writtenSimulated with an 8-bit type and data_i = 5a for four cycles, the two simulators disagree, and neither warns:
Icarus: STAGES=0: data_i=5a data_o=xx
Verilator: STAGES=0: data_i=5a data_o=00The bug surfaces far from its cause: X in one simulator, zeros in the other, and whatever the synthesis tool decides in silicon.
The fix checks the parameter before any storage is declared. An illegal value now stops the build with a message that names the module and the rule.
module pipeline #(
parameter type T = logic [31:0],
parameter int STAGES = 2
) (
input logic clk,
input T data_i,
output T data_o
);
if (STAGES < 1) begin : g_bad_stages
$error("pipeline: STAGES must be at least 1"); // elaboration-time check
assign data_o = 'x; // keeps the bad build well-formed
end else begin : g_pipeline
T stage_q [0:STAGES-1];
always_ff @(posedge clk) begin
stage_q[0] <= data_i;
for (int i = 1; i < STAGES; i++) stage_q[i] <= stage_q[i-1];
end
assign data_o = stage_q[STAGES-1];
end
endmoduleparameter type T passes a whole data type: width, signedness and any packed struct layout travel together.
if (STAGES < 1) at module scope is a generate-if: it is decided while the design is built, not while it runs.
$error inside that branch is an elaboration system task, so the build stops before any simulation or synthesis.
assign data_o = 'x gives the illegal branch a driver, so the only message is the one you wrote.
g_pipeline holds the real storage, declared only when STAGES is legal.
With STAGES = 0 the same instance now fails to build:
Icarus:
ERROR: pipeline.sv:10: pipeline: STAGES must be at least 1
During elaboration Scope: g_bad_stages
Verilator:
%Warning-USERERROR: pipeline.sv:10:5: pipeline: STAGES must be at least 1Configuration | Result | Why |
|---|---|---|
STAGES = 0 or negative | Build stops with the guard message | No storage range exists; unguarded it was [0:-1] |
STAGES = 1 | Legal: one register, one cycle of latency | Minimum value; the copy loop runs zero times |
STAGES = 2 (default) | Legal: two cycles of latency | The configuration the module was written for |
STAGES = 3 | Legal: three cycles of latency | A non-default value; test it as well |
T = logic [7:0] or a packed struct | Legal | Any packed type with a fixed width |
Test at least the minimum, a normal value and one more. Each row above is one elaboration of the same source.
Use localparam for derived values callers must not override.
Guard zero/one values before $clog2 or part-select expressions.
Test minimum, non-power-of-two, and normal configurations.
Do not use defparam; named parameter overrides keep hierarchy ownership visible.
The type carries width, signedness, and packed structure together, preventing callers from choosing inconsistent combinations.
The output reads stage_q[-1], an entry nothing writes. Its value is the simulator's default for an unwritten variable: X in a four-state simulator, 0 in a two-state one. Neither is a design decision, which is why the guard rejects the configuration.
Unguarded STAGES = 0 run for four cycles with data_i = 5a: Icarus Verilog 13.0 printed data_o = xx and Verilator 5.048 printed 00 (2026-10-17); Verilator lint with -Wall reported nothing about the range. The guarded module stopped at elaboration in both tools with the messages shown.