How the heck does unparameterization/elaboration work?
Our algorithm processes a list of signatures that specify what modules we want to create with what parameters.
We start from the top-level modules. Any module that is never instantiated is assumed to be a top-level module, and we'll use its default parameters (if it has parameters). The initial signature list has the top level modules, associated with their default parameters.
Recursively, to create a module with a set of parameters, we apply vl-genblob-resolve, which substitutes the parameter values everywhere they're needed, and then examines the module instances. For each module instance that has parameters, we add to our list of signatures the needed module, and replace that instance with a parameter-free instance of that module. (The function that does this on each instance is vl-unparam-inst.)
When we're done with the current module, we recur on the list of signatures we accumulated; when we're done with those, we've completely unparameterized the module and all of its dependencies. This is what vl-unparameterize-main does.
When we unparameterize a module instance like
module supermod ; ... foo #(.size(64), .kind(foo_t)) myinst (...) ; endmodule
The actual parameter arguments (64, foo_t) need to be resolved in the scope
of supermod. But then, we are going to create a signature for specializing
To accomplish this, for value parameters (e.g. size) we insist that the each
value gets resolved to a literal (e.g., 64, "foo", etc.), and such literals
are of course independent of scope. If we can't resolve a parameter, e.g., you
For type parameters the situation is harder. We can't simply replace a named type like foo_t with its definition (recursively), because various rules in SystemVerilog prohibit that, especially pertaining to signedness.
(Digression: e.g., consider the type foo_t:typedef logic signed [3:0] [5:0] signedarr_t; typedef signedarr_t [7:0] foo_t;
You can't define foo_t without a subsidiary typedef, because there's no way to directly express a multidimensional array of which some inner dimension is signed.)
So what to do? Our approach is to annotate the named type (see vl-usertype) with its definition as the
In summary: the actual values for type parameters in our signatures are
scope agnostic in the sense that we know we can't trust their names, and in
the sense that we don't need their names to know anything about them, because
we have these
If a module is instantiated twice with the same parameters, we don't want to create two different unparameterized versions of that module. However, we need to be careful not to share a definition for two modules whose parameters look the same, but are actually different. E.g., a module with a type parameter may be instantiated twice with two types that have the same name, but refer to different definitions because they occur in different scopes.
To keep track of this, we create a key for each module instantiation. Two module instantiations should produce the same key only if they are instantiations of the same module, all value parameters resolve to the same values, and all type parameters are the same up to and including the scope in which any usertypes are defined. To track which scope is which, we use vl-scopestack->hashkey, which reduces a scopestack to a hierarchy of names.
If an interface is instantiated with a set of parameters and then passed to a module instance, that instance differs from a similar instance that is passed an interface instantiated with different parameters, even if the parameters two the two instances are the same (or there aren't any parameters).