Support for handling warnings and errors.
Many parts of VL can run into situations where we want to issue a warning or
cause an error. For instance:
- Our loader could encounter syntax that is simply malformed, or it
could run into a construct that is legal but that we don't support yet. It
could also notice valid but strange cases, e.g., 4'd16 is well-defined but
weird because 16 doesn't fit in 4 bits.
- Our transforms might run into semantically ill-formed constructs,
e.g., perhaps a module declares wire [3:0] foo and then later declares
integer foo, or perhaps we are trying to instantiate a module that is not
- Our lint checks might notice "code smells" where even though the
input Verilog is semantically well-formed, it is somehow strange and looks like
it could be an error. For instance, perhaps there are multiple assignments to
the same wire, or expressions like a & b where a and b have
Handling these many kinds of cases is tricky. In the early days of VL, our
approach to warnings and errors was quite ad-hoc. We sometimes printed warning
messages to standard output using the cw function. For more serious
conditions, we sometimes caused errors using er. This approach had a
number of problems. In particular,
- It led us to see many of the same warnings repeatedly because our various
well-formedness checks were run many times on the same modules in different
stages of the translation.
- For some warnings, we did not particularly care about the individual
instances of the warning. For instance, unless we're interested in fixing the
"if" statements to be ?: instead, we don't want to be told about each
occurrence of an if statement. We just want a higher-level note that hey,
there are 30 if-statements to clean up.
- The warnings were not "attached" in any way to the modules that they were
actually about. Practically speaking, this might mean that users might not
even see the warnings that had been generated for the modules they are working
- There is no way to recover form an error created with er, so if we
ran into some bad problem with a particular module, it could actually prevent
us from translating any of the modules. This was particularly
troublesome because Verilog is such a large language that, especially in the
beginning, we often ran into constructs that we did not yet support.
These sorts of problems quickly led us to want a more coherent, global
approach to dealing with warnings and errors.
Our new approach to warning and error handling centers around explicit vl-warning-p objects.
These objects are in many ways similar to the Exception objects
found in other programming languages. Each warning has a type and a
message that describes the error. These messages can conveniently make
use of VL's printer, so you can directly pretty-print arbitrary Verilog
constructs when writing warning messages.
We use vl-warning-p objects universally, for all kinds of warnings and
errors. That is, everything from the most minor of code smells (wire foo
is never used for anything), to the most severe problems (the module you're
instantiating isn't defined) results in a warning. Since it is useful to
distinguish minor commentary from severe problems, our warning objects include
a fatalp field.
As a general philosophy or strategy for using these warning objects:
- Warnings messages should never be printed to standard output. Instead, we
should create a vl-warning-p object that gives the context, and explains
the problem as clearly and concisely as possible.
- Errors should not cause sudden, unrecoverable exits. That is, er
should never be used for warnings that could plausibly be triggered by
malformed Verilog. (However, it is reasonable to use er in an assert-like
fashion, to rule out programming problems that we believe are impossible.)
- Non-fatal warnings should be used for any issues that are purely stylistic,
"code smells," etc., such as linter-like checks.
- Fatal warnings should be used for any issues that are truly errors. For
instance: malformed syntax, conflicting declarations of some name, references
to undefined modules, etc.
- Fatal warnings may also be used when a transform encounters constructs that
are valid but not supported, e.g., because we have simply not yet spent the
time to implement them.
Warning objects are simple enough to understand, but what do we actually
do with them? We adopt another general principle:
- Every warning object should be associated with the top-level design
elements (e.g., module, package, interface, etc.) where it was caused.
This approach allows us to easily do many practically useful things with the
warnings. For instance, it lets us easily filter out any modules that have
fatal warnings—an important operation for vl-simplify. As another
example, we can create reports, e.g., about all of the warnings for some
particular module, or all the warnings of some particular type throughout all
of the modules, etc. These capability is used in tools like vl-lint.
Practically implementing this philosophy is slightly tricky.
Deep within some particular transform, we might encounter something that is
wrong and decide to issue a warning. In a typical object-oriented programming
language, this would be trivial: our module class might have an
add-warning that (destructively) adds a new warning to the module.
But our programming language is truly functional, so we cannot modify
existing modules. Instead, whenever some subsidiary function wants to produce
a warning, its caller must take measures to ensure that the warning is
eventually added to the appropriate module.
Our usual approach is to add a warnings accumulator as an argument to
our functions. Typically this argument should be named warnings.
Functions that take a warnings accumulator return the (possibly extended)
accumulator as one of their return values. Macros like ok, warn
and fatal can assist with implementing this convention.
At a high level, then, a function that transforms a top-level design
element, e.g., a module, begins by obtaining the current warnings for the
module. Using these warnings as the initial accumulator, it calls its
subsidiary helpers to carry out its work. This work transforms various parts
of the module, and meanwhile the warnings are perhaps extended. Finally, the
function returns a new vl-module-p which is updated with the extended
list of warnings.
- (vl-warninglist->types x) maps vl-warning->type across a list.
- Fundamental warning object used throughout vl2014.
- The error handling strategy used by vl-simplify.
- A (typically fast) alist associating names to warnings.
- Mergesort warnings using vl-warning-<
- An attribute- mechanism for suppressing particular warnings
when using lint.
- A transform to clean up all the warnings in a design.
- Extend a warnings accumulator with a non-fatal warning.
- Check if any warning is marked as fatal.
- Pretty-print a vl-warninglist-p with a header saying how many
warnings there are.
- Extract flat lists of warnings from various design elements.
- Keep only warnings of certain types.
- Remove warnings of certain types.
- Pretty-print a vl-warninglist-p.
- Check if there are any warnings of certain types.
- Sort and remove duplicates from a list of warnings.
- Pretty-print a vl-warninglist-p into a string.
- A list of vl-warning-p objects.
- Pretty-print a vl-warning-p.
- A convenient shorthand for calling vl-warninglist-fix.
- Pretty-print warnings as they are created.
- Extend a warnings accumulator with a fatal warning.