SystemVerilog loops in functions are completely broken
While writing a ternary adder tree (code below and in attached project archive), I wrote a constant function (IEEE 1800-2023 section 13.4.3) that uses a loop to calculate parameter values for lower level instances based on the given parameter. The code worked well in Vivado and Verilator, but is totally broken in Quartus Prime Lite 24.1.
The first problem is the loop not terminating on return, causing elaboration errors:
Error (10106): Verilog HDL Loop error at ternary_adder_tree.sv(15): loop must terminate within 5000 iterations
Error (10903): Verilog HDL error at ternary_adder_tree.sv(44): failed to elaborate task or function "smaller_pow_3"
Error (10192): Verilog HDL Defparam Statement error at ternary_adder_tree.sv(44): value for parameter "MaxSubN" must be constant expression
Error (12153): Can't elaborate top-level user hierarchy
The loop should terminate during second iteration (N = 9):
- p = 1; next_p = 3; cond false
- p = 3; next_p = 9; cond true - return from function with value 3
If the while (1) loop is replaced with an empty-body for (commented out in code), the loop terminates, but the function somehow returns a value that is not constant and also causes the 10192 error:
Warning (10242): Verilog HDL Function Declaration warning at ternary_adder_tree.sv(24): variable "p" may have a Don't Care value because it may not be assigned a value in every possible path through the statements preceding its use
Error (10192): Verilog HDL Defparam Statement error at ternary_adder_tree.sv(44): value for parameter "MaxSubN" must be constant expression
The warning (10242) emitted before the error is a false positive, because initialisation in for-loop construct is not conditional and in fact is the first thing that happens. Moving the initialisation of p to its declaration gets rid of the warning, but the error remains.
Replacing the loop with multiple if (x <= X) return Y; lines works, but is not scalable and prone to errors. Are loops in functions for some known reason broken/forbidden or am I doing something wrong? I've already seen loop generate constructs breaking things in Quartus, but I hope this time manual copy&paste is not necessary.
// Uncomment the line below to use a version that works in Quartus // `define NO_LOOP module ternary_adder_tree #( parameter int WIDTH = 8, parameter int N = 9 ) ( input logic [WIDTH-1:0] inputs[N], output logic [WIDTH-1:0] sum ); function automatic int smaller_pow_3(input int x); `ifndef NO_LOOP int p = 1; int next_p; while (1) begin next_p = p * 3; if (next_p >= x) return p; p = next_p; end // This doesn't work either, but is not "infinite" // int p; // for (p = 1; p*3 < x; p *= 3); // return p; `else // Quartus-compatible version if (x <= 3) return 1; if (x <= 9) return 3; if (x <= 27) return 9; if (x <= 81) return 27; if (x <= 243) return 81; if (x <= 729) return 243; if (x <= 2187) return 729; if (x <= 6561) return 2187; if (x <= 19683) return 6561; if (x <= 59049) return 19683; return 'x; `endif endfunction function automatic int min(input int a, input int b); return a <= b ? a : b; endfunction localparam int MaxSubN = smaller_pow_3(N); localparam int Sub1N = MaxSubN; localparam int Sub2N = min(MaxSubN, N - Sub1N); localparam int Sub3N = min(MaxSubN, N - Sub1N - Sub2N); // Quartus for some reason requires generate keyword (conditional generate constructs are not supported outside generate regions) generate if (N == 3) assign sum = inputs[0] + inputs[1] + inputs[2]; if (N == 2) assign sum = inputs[0] + inputs[1]; if (N == 1) assign sum = inputs[0]; if (N > 3) begin : g_adder_subadders logic [WIDTH-1:0] s1; logic [WIDTH-1:0] s2; ternary_adder_tree #(.WIDTH(WIDTH), .N(Sub1N)) i_at3_1(.inputs(inputs[0:Sub1N-1]), .sum(s1)); ternary_adder_tree #(.WIDTH(WIDTH), .N(Sub2N)) i_at3_2(.inputs(inputs[Sub1N:Sub1N+Sub2N-1]), .sum(s2)); if (Sub3N > 0) begin : g_adder_sub_full3 logic [WIDTH-1:0] s3; ternary_adder_tree #(.WIDTH(WIDTH), .N(Sub3N)) i_at3_3(.inputs(inputs[Sub1N+Sub2N:N-1]), .sum(s3)); assign sum = s1 + s2 + s3; end else begin : g_adder_sub_only2 assign sum = s1 + s2; end end endgenerate endmodule
Working version
After trying multiple different ways, I found a working version:
function automatic int smaller_pow_3(input int x);
smaller_pow_3 = 1;
while(smaller_pow_3 * 3 < x) smaller_pow_3 *= 3;
endfunctionThis has:
- while instead of for
- no local variables
- no return statement (return value assigned to function name)
Not working versions
Other technically correct, but not working versions (based on the working one):
1. local variable + (optional) return
function automatic int smaller_pow_3(input int x);
int p = 1;
while (p * 3 < x) p *= 3;
return p;
// or
// smaller_pow_3 = p;
endfunctionUsing a separate local variable makes Quartus think that the loop does not terminate. It does not matter whether a return statement or assignment to function name is used - just using a variable breaks synthesis.
2. Equivalent for loop
function automatic int smaller_pow_3(input int x);
for (smaller_pow_3 = 1; smaller_pow_3 * 3 < x; smaller_pow_3 *= 3)
;
endfunctionAccording to Verilog-2001 standard (IEEE 1364-2001, section 9.6) this should behave exactly like the working version with while loop, but in Quartus it warns that the function may return Don't Care (10241) and exits with an error, because the returned value is not a constant expression (10192).
Conclusions
I'm not sure why the not working versions don't, work, but if someone else has this problem (in SystemVerilog or Verilog) try the following.
In Quartus Prime Standard/Lite:
- don't use for loops - try an equivalent while
- don't use local variables in functions
- if you need a variable to return, use assignment to function name (even as temporary variable)