Skip to content

Verilog: loop-local genvar scoping - #2091

Draft
kroening wants to merge 4 commits into
mainfrom
kroening/fix-loop-local-genvar
Draft

Verilog: loop-local genvar scoping#2091
kroening wants to merge 4 commits into
mainfrom
kroening/fix-loop-local-genvar

Conversation

@kroening

@kroening kroening commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Per 1800-2017 27.4, a genvar that is declared in the header of a loop generate
construct is local to that loop. Two loops in the same scope may hence both
declare a genvar with the same name:

for (genvar gi = 0; gi < 2; gi++) begin : b1 ... end
for (genvar gi = 0; gi < 2; gi++) begin : b2 ... end

This fixes the KNOWNBUG added in #2085, which previously failed with
definition of symbol `gi' conflicts with earlier definition.

Root cause

elaborate_generate_decl created a symbol for the genvar using
hierarchical_identifier(base_name), i.e. in the scope that encloses the loop,
irrespective of whether the genvar was declared in the loop header or
separately. The second loop hence tried to insert a symbol that was already
there.

Approach

A genvar that is declared in the header of a loop generate construct is now
recognised as loop-local by elaborate_generate_for, which calls
elaborate_generate_decl with a new loop_local flag. Such a genvar is no
longer added to the enclosing scope. Its value is tracked in the genvars
environment, which is what the genvar's uses are now resolved against. This
works both while elaborating the loop and when type checking the elaborated
module items, since these carry the genvar state in the set_genvars items that
the elaboration produces already.

Once the loop has been elaborated, the loop-local genvars are removed from the
genvar environment, i.e. they go out of scope, restoring any genvar of the same
name that they shadowed (nested loops). A reference to the genvar after the loop
is now rejected (genvar_scope4).

Resolution order

Since a loop-local genvar has no symbol, but may well have the same base name as
a symbol that is visible from the scope that contains the loop, the genvar
environment takes precedence over such a symbol for the extent of the loop, i.e.
in the loop condition, the iteration expression, and the loop body:

wire [7:0] gi;              // shadowed by the genvar below
if (1) begin : outer
  for (genvar gi = 0; gi < 2; gi++) begin : inner ... end
end

To that end, the genvar environment records, for each genvar that is local to a
loop, the scope that contains that loop. A reference resolves to the genvar
when the symbol that resolve finds is not declared inside the loop; a symbol
declared inside the loop, e.g. a function argument, still wins. Outside of the
loop the enclosing symbol is found as before.

Genvars during the elaboration of module instances

The elaboration of module instances, of their parameter values, and of parameter
overrides recurses through the set_genvars module items that the elaboration of
the generate constructs produces, but did not restore the genvar environment
when doing so. This went unnoticed for genvars that have a symbol, as resolve
finds these (albeit with the value the genvar has once the loop has terminated),
but a loop-local genvar has no symbol and hence failed to resolve at all:

for (genvar g = 0; g < 4; g++) begin : blk
  child c(.i(d[g]));           // unknown identifier g
end

elaborate_module_instances, parameterize_instantiated_modules and
process_parameter_override now restore the genvar environment from the
set_genvars item, in the same way #2067 does for the first two of these. As a
side effect, port connections and parameter values that use a genvar which is
not loop-local now also see the value the genvar has in the given iteration,
which is the bug that #2067 fixes; generate/generate-inst3 passes with this
change, but is left as KNOWNBUG here so that #2067 flips it.

Header-declared vs. separately declared genvars

Only genvars declared in the loop header are loop-local. A genvar that is
declared separately, e.g. genvar gi; in the module body, remains a symbol in
the enclosing scope and may be shared by several loops; that path is unchanged
(genvar_scope2).

A loop-local genvar that clashes with a symbol that is already present in the
scope that contains the loop is still reported as a conflict, with the same
message as before (genvar_scope3).

Tests

  • generate/genvar_scope1 is now CORE, and checks that the two loops yield
    independent instances with the expected values (main.b1[i].w is i,
    main.b2[i].w is 10+i).
  • generate/genvar_scope2 (new): a module-scoped genvar shared by two
    sequential loops.
  • generate/genvar_scope3 (new): a loop-header genvar clashing with a wire in
    the same scope as the loop is still an error.
  • generate/genvar_scope4 (new): the loop-header genvar is not visible after
    the loop.
  • generate/genvar_scope5 (new): a loop-header genvar shadows a wire with the
    same name in a scope that encloses the loop.
  • generate/genvar_scope6 (new): likewise for a localparam and for a genvar
    that is declared in an enclosing scope.
  • generate/genvar_scope7 (new): two nested loops both declaring a genvar with
    the same name in their header.
  • generate/genvar_scope8 (new): a loop-local genvar in the port connections
    (input and output) and in the parameter values of a module instance.

regression/verilog (test and test-z3), regression/ebmc,
regression/smv, regression/vlindex and the unit tests all pass.

Per 1800-2017 27.4, a genvar that is declared in the header of a loop
generate construct is local to that loop. Two loops in the same scope may
hence both declare a genvar with the same name.

Such genvars are no longer added to the enclosing scope; their value is
tracked in the genvar environment, which is also what references to them
are now resolved against, and they are removed from that environment once
the loop has been elaborated. Genvars that are declared separately from
the loop are unaffected, and remain shared between loops.

This fixes the KNOWNBUG added in #2085.
A genvar that is declared in the header of a loop generate construct is local
to that loop, 1800-2017 27.4.  It does not have a symbol of its own, and hence
a reference to it must be resolved using the genvar environment rather than the
symbol table.  Any symbol with the same base name that is visible from the
scope that contains the loop is shadowed for the extent of the loop, i.e., in
the loop condition, the iteration expression, and the loop body.

The genvar environment now records, for each genvar that is local to a loop,
the scope that contains that loop.  A reference resolves to the genvar when the
symbol that resolve() finds is not declared inside the loop.  Genvars that are
declared separately from the loop generate construct are unaffected, as these
do have a symbol.
The elaboration of module instances, of their parameter values, and of
parameter overrides recurses through the set_genvars module items that the
elaboration of the generate constructs has produced, but did not restore the
genvar environment when doing so.  This went unnoticed for genvars that are
declared separately from the loop generate construct, as these have a symbol
that resolve() finds, albeit with the value the genvar has once the loop has
terminated.  A genvar that is declared in the header of the loop generate
construct is local to that loop, 1800-2017 27.4, and does not have a symbol,
and hence failed to resolve altogether.

The genvar environment is now restored from the set_genvars module item in
elaborate_module_instances, parameterize_instantiated_modules, and
process_parameter_override, which also yields the value the genvar has in the
given iteration of the loop.
The loop-local genvar fix makes the genvar in the part selects of the
port connections evaluate per iteration, and hence this test now
passes.  Leaving it as KNOWNBUG fails the KNOWNBUG checks CI job,
which runs test.pl -K and expects KNOWNBUG tests to fail.
@kroening

Copy link
Copy Markdown
Collaborator Author

Note on overlap with #2067: both PRs flip `regression/verilog/generate/generate-inst3.desc` from KNOWNBUG to CORE, and #2067 additionally adds `regression/verilog/port-connections/output_port_genvar1.{sv,desc}` as a CORE test for a genvar-indexed output port connection in a generate loop, which this PR also fixes. Whichever lands second will need a trivial conflict resolution in `generate-inst3.desc` and in `verilog_elaborate_module_instances.cpp`.

@kroening
kroening marked this pull request as draft August 12, 2026 15:03
@kroening

Copy link
Copy Markdown
Collaborator Author

I am contemplating whether it would not be easier to simply create an unnamed scope for each generate-for.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant