If there are hardware engineers who love Verilog, I haven’t met them. Almost universally, the attitude toward Verilog seems to be that it’s frustrating, ridiculous, error-prone, and the only pragmatic choice.
Verilog is inescapable because it is the input format to essentially every EDA tool. Its centrality means that it is a de facto intermediate representation implementing for every other HDL: even if you prefer Bluespec, Chisel, Amaranth, or Spade, they all have to compile to Verilog to interact with the rest of the hardware world.
I am worried that Verilog’s flaws will be the cause of a new wave of hardware bugs. There is an analogy to the problems with C and C++: as designing custom hardware becomes more popular, we risk allowing a dangerous HDL to proliferate and fester in the same way that memory-unsafe programming languages have in software.
I don’t know yet what the analog of memory safety is for hardware bugs, or even if there will be a single dominant defect category. Footguns abound in Verilog, though, so there are many good candidates. This post makes the case that we should invest in better understanding the problems with Verilog so that future HDLs can avoid them.
On Building Blocks
As an American programming languages nerd, I believe that “Back to the Building Blocks” was one of the most exciting developments of the last decade. It’s a 2024 report from the White House Office of the National Cyber Director that makes the case for memory safety. It calls for critical infrastructure to move on from memory-unsafe languages like C and C++, and it even mentions Rust by name as a promising alternative.
“Back to the Building Blocks” didn’t break new ground: by 2024, it was obvious to all reasonable people that memory safety was a huge problem. It was exciting because Joe Biden was saying the same things that we had all been saying for years.1 I’m not a very patriotic person, but my heart soars like a majestic bald eagle when I read stuff like this in a government report:
Despite rigorous code reviews as well as other preventive and detective controls, up to 70 percent of security vulnerabilities in memory unsafe languages patched and assigned a CVE designation are due to memory safety issues.
And I can hear the national anthem playing when the report says:
For new products, choosing to build in a memory safe programming language is an early architecture decision that can deliver significant security benefits. Even for existing codebases, where a complete rewrite of code is more challenging, there are still paths toward adopting memory safe programming languages by taking a hybrid approach.
God bless America. “Back to the Building Blocks” distills a hard-to-refute syllogism along these lines:
- Correctness matters.
- There exist large classes of bugs with similar root causes.
- Some languages lead to a higher frequency of these bug classes than other languages.
- We should therefore see these bugs as the “fault” of the language, not the programmer.
- Languages that are harder to use but dramatically reduce the frequency of these bugs may be worth it.
In the original report, this “Building Blocks” argument was about C. But I believe the same reasoning applies to Verilog.
The Claim (Weak and Strong Forms)
I’ll state my thesis in a weak form and a strong form; you can pick which level you want to consider. The weak form is:
Verilog causes lots of bugs.
And the strong form is:
The next “Building Blocks” crisis will happen in hardware, and it will be Verilog’s fault.
In other words: at the same moment that we’re beginning to get a handle on memory safety, we’re producing a lot more Verilog code. So the next scourge of avoidable bugs could occur in hardware. Better HDLs could dramatically reduce the frequency of these bugs.
Verilog’s problems are not new, but until now, hardware design methodologies have attenuated their harm. The traditional way to develop hardware—the kind of process that big CPU vendors use—mitigates Verilog’s problems in part by expending a ridiculous amount of resources on verification. This observation is hard to justify with concrete evidence, but consider this somewhat dubious report from an industry consortium that claims that, in CPU design projects, the ratio of verification engineers to design engineers is 5:1. A terrible HDL matters less when you have a safety net like that.
But today, more people want to design custom, application-specific hardware. Specialized, lower-volume hardware projects will not (and should not) use the same engineering process as Apple’s next iPhone SoC. The emerging long tail of cheaper, lighter-weight hardware design projects will be more vulnerable to Verilog’s problems.
Some Cheap Shots at Verilog
The main point of this post is to call for systematic study of Verilog’s implications for hardware correctness, not to pick on specific flaws I personally love to hate. But I can’t resist shooting a few fish in this barrel.
The root of Verilog’s problems is that it was not designed for implementing hardware. It was originally developed as a DSL for writing event-based simulators of digital logic. Later, logic synthesis tools repurposed Verilog for generating real netlists. Many problems with Verilog stem from the confusing boundaries between simulation and implementation:
- There is an ill-defined “synthesizable” subset. Tools can’t agree on what this subset is, but we can all agree that not all of Verilog can be sensibly translated into hardware.
- Verilog requires load-bearing linters. Serious hardware design shops pay for extremely expensive commercial tools that keep their engineers within the Verilog subset that their toolchain can handle.
To get concrete, let’s look at three specific footguns in Verilog: inferred latches, the semantics of the “don’t care” value, and the absence of cycle-level timing information. (As a warning, the latter is a self-serving complaint that motivates some research I co-authored.)
Inferred Latches
Verilog, people say, uses the register-transfer level (RTL) abstraction. If this was the first thing I knew about Verilog, I would assume it works like this: the language has special, built-in constructs for state elements like latches, registers, and memories. You write a Verilog program by describing how to compute the values of all state elements on cycle n+1 based on their values on cycle n. In an RTL language, it seems obvious that all the registers should be explicit and clearly separated from stateless combinational logic.
Because of Verilog’s roots in event-based simulation, that’s not how it works. Instead, variables might be stateful and might be stateless depending on how they are used. For example, here’s one way to write a 32-bit latch in Verilog:
module latch (
input wire en,
input wire [31:0] data_in,
output reg [31:0] data_out
);
always @(*) begin
if (en)
data_out = data_in;
end
endmodule
There’s an input data port, an output data port, and a 1-bit enable signal that decides whether the latch should take on a new value.
The Verilog semantics say that data_out must be stateful because it is assigned conditionally.
That is, because data_out doesn’t get assigned on cycles when en == 0, it keeps its old value—implying that it requires a stateful circuit for its implementation.
Verilog engineers call this an “inferred latch.”2
The footgun here is that it’s disturbingly easy to accidentally create a latch. Consider this contrived implementation of an XOR gate:
module funny_xor (
input wire [1:0] in_bits,
output reg out
);
always @(*) begin
if (in_bits == 2'b00)
out = 1'b0;
else if (in_bits == 2'b01)
out = 1'b1;
else if (in_bits == 2'b10)
out = 1'b1;
else if (in_bits == 2'b11)
out = 1'b0;
end
endmodule
This module takes its two inputs on a single 2-bit port, in_bits.
This circuit is stateless because our else if cascade covers all the cases:
out is assigned in every situation.
But what happens if you forget one of these cases? For example, let’s delete these two lines:
else if (in_bits == 2'b10)
out = 1'b1;
The module is now stateful, and the hardware implementation requires a latch circuit. Spooky.
Nonsense Semantics for X
HDLs typically need a way to represent don’t-care values, usually written as X.
A little like undefined behavior (in a good way), X lets you convey to the optimizer that your specification only covers certain cases, and that it’s free to do what it wants in others.
While X is a good and useful idea, Verilog’s semantics for it don’t make sense.
Here’s how it should work: X represents a bit that might be 0 or 1, and we don’t know which.3
By that definition, it would make sense that X would propagate through Verilog’s math operators, like this:
module optimism1;
reg [31:0] in;
reg [31:0] out;
initial begin
in = 32'bx;
$display("in = %d", in);
out = in * 2 + 4;
$display("out = %d", out);
end
endmodule
And indeed, simulating this module shows that out is also a don’t care value, just like in:
$ iverilog optimism1.v && ./a.out
in = x
out = x
All is right with the world.
Verilog also has a ternary operator, and we can try using X in its condition:
out = (in * 2 + 4) > 42 ? 32'b1 : 32'b0;
Gratefully, the result of an undefined condition is itself X:
$ iverilog optimism2.v && ./a.out
in = x
out = X
Because out might be 1 and it might be 0, it’s sensible that our simulator reports is value is X.
Let’s go one step farther and rewrite that ternary operator using the long-form if equivalent:
if ((in * 2 + 4) > 42)
out = 32'b1;
else
out = 32'b0;
And try simulating again:
$ iverilog optimism3.v && ./a.out
in = x
out = 0
This footgun has a name: X-optimism.
That’s the standard behavior for Verilog: if treats X as false, which incorrectly makes conditional assignments appear defined when they are actually unknown.
Tragically, this behavior was apparently intentional:
This optimistic behavior of if-else was a deliberate decision by Moorby. He realized that in such a procedural context, evaluating both sides of the if-else expression could be enormously complicated. Reducing optimism requires execution time in the simulator.
Feel free to call me a PL nerd for saying this, but I don’t think simulation performance is a good justification for broken semantics.
Unsurprisingly, researchers have explored how X optimism can lead to sneaky security problems because your simulator lies to you about what how final circuit will behave.
Timing Trouble
This last footgun is a little different because it’s the problem we have tried to address in some of our recent research on safer HDLs (and it’s borrowed from that paper). It’s not just a Verilog thing; it’s a danger in every major HDL I know of. It’s also my best guess at this point for a bug category that’s analogous to memory safety problems in software.
Let’s say you already have a multiplier and an adder:
module Mul32 (
input wire [31:0] in_a,
input wire [31:0] in_b,
output reg [31:0] out,
);
// ...
endmodule
module Add32 (
input wire [31:0] in_a,
input wire [31:0] in_b,
output reg [31:0] out
);
// ...
endmodule
Suppose you want to combine these two functional units into an ALU that can both add and multiply.
We’ll add a one-bit signal, op, that picks which FU to use:
module alu (
input wire [31:0] in_a,
input wire [31:0] in_b,
input wire op,
output reg [31:0] out
);
wire [31:0] add_out;
wire [31:0] mul_out;
Add32 add (.in_a(in_a), .in_b(in_b), .out(add_out));
Mul32 mul (.in_a(in_a), .in_b(in_b), .out(mul_out));
always @(*) begin
out = (op == 1'b0) ? add_out : mul_out;
end
endmodule
This module instantiates an adder add and a multiplier mul, and it uses Verilog’s ternary operator to multiplex the output based on op.
This implementation works fine if add and mul are both combinational.
More realistically, though, a 32-bit multiplier will probably be sequential and pipelined.
If that’s the case, this code is probably incorrect:
it has implicitly assumed that Add32 and Mul32 have the same timing behavior.
Here’s the problem: there is nothing in the module signatures for the functional units that tells us that’s the case.
The interfaces for Add32 and Mul32 are identical because they can only describe the physical shape of the ports, not the timing.
Even if Add32 is combinational and Mul32 takes 3 cycles, their Verilog signatures would remain identical.
The only way to know how to use a Verilog module correctly is to read the comments (or to look directly at the source code).
If you’re curious about this particular problem, please take a look at our paper about Filament, an HDL with a type system that enforces timing safety.4
The Building Blocks’ Building Blocks
If low-level systems like compilers are the building blocks for the software industry, then hardware design tools are the building blocks for the building blocks. The tower of system abstractions needs a strong foundation, and Verilog is a cracked and crumbling concrete slab.
We must work toward a post-Verilog future. We need to invest more in modern HDLs, and we need toolchains that can support these superior languages without going through Verilog as the least common denominator.
-
I choose to believe that Biden wrote “Back to the Building Blocks” all by himself, and I am not interested in evidence to the contrary. ↩
-
It would be reasonable to guess that the
regkeyword means thatdata_outis stored in a register. That’s not the case.regdeclares mutable variables that can be either combinational or stateful, depending on how they’re used. ↩ -
In lattice terms,
Xshould work like a top element. ↩ -
Please direct all Filament fan mail to Rachit, who led that work. ↩