Warning-basics
General introduction to vl-warning objects and error handling
in VL.
Introduction
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
defined.
- Our vl-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 different sizes.
Handling these many kinds of cases is tricky. In the earliest 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 ad-hoc
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
on.
- 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.
Warning Objects
Our new approach to warning and error handling centers around explicit vl-warning 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 objects universally, for all kinds of warnings and
errors. That is, everything from the most minor of code smells (e.g., wire
foo is never used for anything), to the most severe problems (e.g., the
module you're instantiating isn't defined) results in a warning. To
distinguish minor oddities from severe problems, our warning objects include a
fatalp field.
As a general philosophy or strategy for using these warning objects:
- Warning messages should never be printed to standard output. Instead, we
should create a vl-warning-p object that provides 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.
Accumulating Warnings
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; see propagating-errors. As another example, we can
create reports such as a vl-reportcard that summarize the warnings in
our design. These kinds of capabilities are especially useful 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.