mirror of https://github.com/llvm/circt.git
[HW Docs] Write rationale for HW modules, parameters and other things.
This resolves Issue 1857
This commit is contained in:
parent
bf7ac80947
commit
039324b830
|
@ -13,8 +13,13 @@ Rationale](RationaleSV.md).
|
|||
- [Introduction to the `hw` Dialect](#introduction-to-the-hw-dialect)
|
||||
- [`hw` Type System](#hw-type-system)
|
||||
- [`hw.module` and `hw.instance`](#hwmodule-and-hwinstance)
|
||||
- [Parameterized Modules](#parameterized-modules)
|
||||
- [Instance paths](#instance-paths)
|
||||
- [Parameterized Modules](#parameterized-modules)
|
||||
- [Valid Parameter Expression Attributes](#valid-parameter-expression-attributes)
|
||||
- [Parameter Expression Canonicalization](#parameter-expression-canonicalization)
|
||||
- [Using parameters in the body of a module](#using-parameters-in-the-body-of-a-module)
|
||||
- [Parameterized Types](#parameterized-types)
|
||||
- [Answers to other common questions](#answers-to-other-common-questions)
|
||||
- [Type declarations](#type-declarations)
|
||||
- [Symbols and Visibility](#symbols-and-visibility)
|
||||
- [Future Directions](#future-directions)
|
||||
|
@ -76,58 +81,259 @@ Verilog.
|
|||
The basic structure of a hardware designed is made up an "instance tree" of
|
||||
"modules" and "instances" that refer to them. There are loose analogies to
|
||||
software programs which have corresponding "functions" and "calls" (but there
|
||||
are also major differences, see "Instance paths" below). A module in the `hw`
|
||||
dialect has several major components:
|
||||
are also major differences, see "[Instance paths](#instance-paths)" below).
|
||||
Modules can have a
|
||||
definition `hw.module`, they can be a definition of an external module whose
|
||||
signature is known but whose body is provided separately `hw.module.extern`,
|
||||
and can be a definition of an external module with a known signature that
|
||||
can/will be generated in the future on demand (`hw.module.generated`).
|
||||
|
||||
1) A symbol `name` which specifies the MLIR name for the module.
|
||||
2) input ports, as bb arguments.
|
||||
3) result ports, with `hw.output`
|
||||
4) parameters (described below)
|
||||
5) other attributes.
|
||||
A simple example module looks like this (many more can be found in the
|
||||
testsuite):
|
||||
|
||||
`hw` is generally designed to be permissive in what it allows for the types of ports.
|
||||
```mlir
|
||||
hw.module @two_and_three(%in: i4) -> (twoX: i4, threeX: i4) {
|
||||
%0 = comb.add %in, %in : i4
|
||||
%1 = comb.add %a, %0 : i4
|
||||
hw.output %0, %1 : i4, i4
|
||||
}
|
||||
```
|
||||
|
||||
TODO: Spotlight on module. Allows arbitrary types for ports.
|
||||
The signature of a modules have these major components:
|
||||
|
||||
**Zero Bit Integer Ports**
|
||||
1) A symbol `name` which specifies the MLIR name for the module
|
||||
(`@two_and_three` in the example above). This is what connects instances to
|
||||
modules in a stable way.
|
||||
2) A list of input ports, each of which has a type (`%in: i4` in the example
|
||||
above). Each input port is available as an SSA value through a block
|
||||
argument in the entry block of an `hw.module`, allowing them to be used
|
||||
within its body. "inout" ports are modeled as inputs with an `!hw.inout<T>`
|
||||
type. Input port names are prefixed with a `%` because they are available
|
||||
as SSA values in the body.
|
||||
3) A list of result port names and types (`twoX: i4` and `threeX: i4` in the
|
||||
example above). In a `hw.module` definition, the values for the
|
||||
results are provided by the operands to the `hw.output` terminator in the
|
||||
body block. The names of result ports are not prefixed with `%` because
|
||||
they are not MLIR SSA values.
|
||||
4) A list of module "parameters", which provide parametric polymorphism
|
||||
capabilities (somewhat similar to C++ templates) for modules. These are
|
||||
described in more detail in the "[Parameterized
|
||||
Modules](#parameterized-modules) section below.
|
||||
5) The `verilogName` attribute can be used to override the name for an external
|
||||
module. We hope to eliminate this in the future and just use the symbol.
|
||||
6) Other ad-hoc attributes. The `hw` dialect is intended to allow open
|
||||
extensibility by other dialects. Ad-hoc attributes put on `hw` dialect
|
||||
modules should be namespace qualified according to the dialect they come
|
||||
from to avoid conflicts.
|
||||
|
||||
Certain operations support zero bit declarations:
|
||||
This definition is fairly close to the Verilog family, but there are some
|
||||
notable differences: for example:
|
||||
|
||||
- The `hw.module` operation allows zero bit ports, since it supports an open
|
||||
type system. They are dropped from Verilog emission.
|
||||
- Interface signals are allowed to be zero bits wide. They are dropped from
|
||||
Verilog emission.
|
||||
|
||||
|
||||
|
||||
### Parameterized Modules
|
||||
|
||||
TODO: describe this.
|
||||
|
||||
- Why not use SSA graph.
|
||||
- Why not default arguments.
|
||||
- Parameterized types.
|
||||
- Concat types.
|
||||
- Canonicalization of affine expressions.
|
||||
- how to go from parameter space to value space with localparam etc.
|
||||
- We split output ports from input ports, don't use `hw.output` instead of
|
||||
connects to specify the results. This allows better SSA dataflow
|
||||
analysis from the `hw.output` which is useful for inter-module analyses.
|
||||
- We allow arbitrary types for module ports. The `hw` dialect is generally
|
||||
designed to be extensible by other dialects, and thus being permissive here
|
||||
is useful. That said, the [Verilog exporter](VerilogGeneration.md) does not
|
||||
support arbitrary user-defined types.
|
||||
- The `comb` dialect in particular does not use signed integer types, its
|
||||
operators do not support zero-width integer types. Modules, on the other
|
||||
hand, do support both of these. Zero width ports are omitted (printed as
|
||||
comments) when generating verilog.
|
||||
|
||||
### Instance paths
|
||||
|
||||
An IR for Hardware is different than an IR for Software in a very important way:
|
||||
while each function in a software program usually compiles into one blob of
|
||||
binary code no matter how many times it is called, each instance in a hardware
|
||||
design is typically fully instantiated, because different instances turn into
|
||||
design is typically fully instantiated, because different instance turn into
|
||||
different gates. The consequence of this is that the instance tree is really a
|
||||
compression mechanism that is eventually elaborated away.
|
||||
|
||||
This compression approach has major advantages: it is much better for memory
|
||||
and compile time to represent a single definition of a hardware block than the
|
||||
(possibly thousands or millions) of concrete instances that will eventually be
|
||||
required. However, hardware engineers do need to reason about and control the
|
||||
different instances in some cases (e.g. providing physical layout constraints
|
||||
for one instance but not the rest).
|
||||
required. However, hardware engineers often do need to reason about and control
|
||||
the different instances in some cases (e.g. providing physical layout
|
||||
constraints for one instance but not the rest).
|
||||
|
||||
TODO: Bake out a design for this.
|
||||
TODO: Bake out a design for instance path references, an equivalent to the
|
||||
FIRRTL dialect `InstanceGraph` type, etc.
|
||||
|
||||
## Parameterized Modules
|
||||
|
||||
The `hw` dialect supports parametric "compile-time" polymorphism for modules.
|
||||
This allows for metaprogramming along the instance tree, guaranteed
|
||||
"instantiation time" optimizations and code generation, further enables
|
||||
the "IR compression" benefits of using instances in the first place, and enables
|
||||
the generation of parameters in generated Verilog (which can increase the
|
||||
percieved readability of the generated code).
|
||||
|
||||
Parameters are declared on modules (including generated and external ones)
|
||||
with angle brackets: each parameter has a name and type, and can optionally
|
||||
have a default value. Instances of a parameterized module provide a value for
|
||||
each parameter (even defaulted ones) in the same order:
|
||||
|
||||
```mlir
|
||||
// This module has two parameters "p1" and "p2".
|
||||
hw.module.extern @parameterized<p1: i42 = 17, p2: i1>(%in: i8) -> (out: i8)
|
||||
|
||||
hw.module @UseParameterized(%a: i8) -> (ww: i8) {
|
||||
%r0 = hw.instance "inst" @parameters<p1: i42 = 17, p2: i1 = 1>(in: %a: i8) -> (out: i8)
|
||||
hw.output %r0 : i8
|
||||
}
|
||||
```
|
||||
|
||||
This approach makes analysis and transformation of the IR simple, predictable,
|
||||
and efficient: because the parameter list on instances and on modules always
|
||||
line up, they are indexable by integers (instead of strings), intermodule
|
||||
analysis is straight-forward (no filling in of default values etc), and
|
||||
Verilog generation is always predictable: the default value for a parameter
|
||||
is used when the instance and the module default are the same (e.g. in the
|
||||
example above, `p1` is not printed at the instance site because it is the same
|
||||
as the default.
|
||||
|
||||
### Valid Parameter Expression Attributes
|
||||
|
||||
The following attributes may be used as expressions involving parameters at
|
||||
an instance site or in the default value for a parameter declaration on a
|
||||
module:
|
||||
|
||||
- IntegerAttr/FloatAttr/StringAttr constants may be used as simple leaf values.
|
||||
- The `#hw.param.decl.ref` attribute is used to refer to the value of a
|
||||
parameter in the current module. This is valid in most positions where a
|
||||
parameter attribute is used - except in the default value for a module.
|
||||
- The `#hw.param.binary` operator allows combining other parameter expressions
|
||||
into an expression tree. Expression trees have important canonicalization
|
||||
rules to ensure important cases are canonicalized to uniquable
|
||||
representations.
|
||||
- `#hw.param.verbatim<"some string">` may be used to provide an opaque blob of
|
||||
textual verilog that is uniqued by its string contents. This is intended
|
||||
as a general "escape hatch" that allows frontend authors to CIRCT does not
|
||||
provide any checking to ensure that this is correct or safe, and assumes it
|
||||
is single expression - parenthesize the string contents if not to be safe.
|
||||
This [should eventually support
|
||||
substitutions](https://github.com/llvm/circt/issues/1881) like
|
||||
`sv.verbatim.expr`.
|
||||
|
||||
Because parameter expressions are MLIR attributes, they are immortal values
|
||||
that are uniqued based on their structure. This has several important
|
||||
implications, including:
|
||||
|
||||
- A parameter reference (`#hw.param.decl.ref`) to a parameter `x` doesn't know
|
||||
what module it is in. The verifier checks that parameter expressions are
|
||||
valid within the body of a module, and that the types line up between the
|
||||
parameter reference and the declaration (after all, two different modules can
|
||||
have two different parameters named `x` with different types).
|
||||
- We want to depend on MLIR canonicalizing and uniquing the pointer address of
|
||||
attributes in a predictable way to ensure that further derived uniqued objects
|
||||
(e.g. a parameterized integer type) is also uniqued correctly. For example,
|
||||
we do not want the types `hw.int<x+1>` and `hw.int<1+x>` to turn into
|
||||
different types. See the [Parameter Expression
|
||||
Canonicalization](#parameter-expression-canonicalization) section below for
|
||||
more details.
|
||||
- Whereas the rest of the `hw` dialect is generally open for extension, the
|
||||
current grammar of attribute expressions is closed: you have to hack the
|
||||
HW dialect verifier and VerilogEmitter to add new kinds of valid expressions.
|
||||
This is considered a limitation, we'd like to move to an attribute interface
|
||||
at some point that would allow dialect-defined attributes. For example, this
|
||||
would allow moving `hw.param.verbatim` attribute down to the `sv` dialect.
|
||||
|
||||
### Parameter Expression Canonicalization
|
||||
|
||||
As mentioned above, it is important to canonicalize parameter expressions. This
|
||||
slightly reduces memory usage, but more importantly ensures that equivalent
|
||||
parameter expressions are pointer equivalent: we don't want `x+1` and `1+x` to
|
||||
be different, because that would cause everything derived from them to be as
|
||||
well.
|
||||
|
||||
On the other hand, we expect to support a lot of weird expressions over time (at
|
||||
least the full complement that Verilog supports) and canonicalize arbitrary
|
||||
expressions in a predictable way is untennable. As such, we support
|
||||
canonicalizating a fixed set of expressions predictably: more may be added in
|
||||
the future.
|
||||
|
||||
This set includes:
|
||||
|
||||
- TODO: None yet. :-)
|
||||
|
||||
### Using parameters in the body of a module
|
||||
|
||||
Parameters are not SSA values, so they cannot directly be used within the body
|
||||
of the module. To project them with a specific name, you can use the
|
||||
`sv.localparam` declaration like so:
|
||||
|
||||
```mlir
|
||||
hw.module @M1<param1: i1>(%clock : i1, ...) {
|
||||
...
|
||||
%param1 = sv.localparam : i1 { value = #hw.param.decl.ref<"param1">: i1 }
|
||||
...
|
||||
sv.if %param1 { // Compile-time conditional on parameter.
|
||||
sv.fwrite "Only happens when the parameter is set\n"
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, if you don't want to introduce a local name, you can use a
|
||||
**TODO**: yet-to-be-implemented new op.
|
||||
|
||||
### Parameterized Types
|
||||
|
||||
TODO: Not done yet.
|
||||
|
||||
### Answers to other common questions
|
||||
|
||||
During the design work on parameterized modules, we had several proposals for
|
||||
alternative designs a lot of discussion on this. See in particular, these
|
||||
discussions at the open design meetings:
|
||||
|
||||
- [September 15, 2021](https://docs.google.com/document/d/1fOSRdyZR2w75D87yU2Ma9h2-_lEPL4NxvhJGJd-s5pk/edit#heading=h.gdy95njn5105):
|
||||
discussion about using SSA values vs attributes for expressions, whether
|
||||
parameters should just be a "special kind of port" etc.
|
||||
- [September 22, 2021](https://docs.google.com/document/d/1fOSRdyZR2w75D87yU2Ma9h2-_lEPL4NxvhJGJd-s5pk/edit#heading=h.tcwfqa9fi7u2):
|
||||
discussions on expression canonicalization, parameterized type casting and
|
||||
other topics.
|
||||
|
||||
This section tries to condense some of those discussions into key points:
|
||||
|
||||
**Why do instances repeat default parameters from modules?**
|
||||
|
||||
As described above, the full set of module parameters are specified on an
|
||||
instance, even if some have default values. The reason for this is that we want
|
||||
the IR to be simple and efficient to analyze by the compiler: keeping (and
|
||||
verifying that) instance parameters are in canonical form means that we can
|
||||
index them with integers instead of names (just like module input and result
|
||||
ports), and intermodule analysis/optimization doesn't have to handle default
|
||||
values as a special case. Instead they are just a matter for frontends and
|
||||
the Verilog exporter to care about.
|
||||
|
||||
**Why model parameters with attributes instead of SSA values?**
|
||||
|
||||
It seems unfortunate to replicate some parts of the `comb` dialect (e.g.
|
||||
`comb.add`) as attributes rather than just reusing the existing attributes.
|
||||
Such a design has historical familiarity (e.g. LLVM's `ConstantExpr` class)
|
||||
which led to a bunch of complexity in LLVM that would have been better avoided
|
||||
(and yes - there are much better designs for LLVM's purposes than what it has
|
||||
now).
|
||||
|
||||
All that said, using attributes is the right thing for a number of reasons:
|
||||
|
||||
1) This arithmetic happens at metaprogramming time, these ops do not turn into
|
||||
hardware. It use important and useful to be able to know that structurally.
|
||||
2) We need to verify parameter expressions are valid for the module they are
|
||||
defined in - it isn't generally ok for the verifier of the `hw.instance` op
|
||||
to walk an arbitrary amount of IR to check that an SSA value is valid as a
|
||||
parameter.
|
||||
3) We need to support parameterized types like `!hw.int<n>`: because MLIR types
|
||||
are immortal and uniqued, they can refer to attributes but cannot refer to
|
||||
SSA values (which may be destoyed).
|
||||
4) Operations need to be able to compute their own type without creating other
|
||||
operations. For example, we need to compute that the result type of
|
||||
`comb.concat %a, %b : (i1, !hw.int<n>)` is `!hw.int<n+1>` without introducing
|
||||
a new `comb.add` node to "add one to n".
|
||||
5) In practice, comb ops and the canonicalizations that apply to them have very
|
||||
different goals than the canonicalizations we apply to parameter expressions.
|
||||
|
||||
## Type declarations
|
||||
|
||||
|
|
2
llvm
2
llvm
|
@ -1 +1 @@
|
|||
Subproject commit 47cc166bc023b497bdffe0964d80f15eaee8b7da
|
||||
Subproject commit 7acd1807dd6899441cff9e1246155379971352fb
|
Loading…
Reference in New Issue