Find the cause of RTL and netlist disagreements
RTL simulation can disagree with a Yosys generic (proc/opt) netlist because the two executions preserve different parts of the source’s meaning. An incomplete event list, a scheduling race or an unknown-value decision can disappear or change when processes become logic and registers. Reduce the disagreement to a small input sequence, keep the tool configuration with the result, and check the corrected requirement separately from agreement between tools.
Start with the same inputs and a specific expectation
The example below puts five categories in one small design: an incomplete sensitivity list, unsigned extension where signed extension was intended, unknown control through if and casex, a blocking-assignment race, and an initial value without a reset response. Each has an adjacent alternative that makes the intended operation explicit. The testbench supplies the same input sequence to both members of each pair.
The words “bad” and “fixed” describe the stated intent of these examples. They do not mean a compiler is wrong whenever the outputs differ. Zero extension is valid for an unsigned input; it is a mistake only when the requirement expects a negative value to retain its sign. Likewise, a synthesis transformation may preserve a two-state function while changing what a four-state simulation shows for an unknown control.
The simulator observations dataset provides additional bounded language examples. Keep its existing results separate from this page’s new runs. The files here form a smaller paired experiment with a Yosys generic (proc/opt) counterpart and a named equivalence obligation, rather than a new row silently added to that dataset.
The complete design and testbench
Save these two files together. The example README gives the commands, exact source commits and recorded configuration. The Yosys generic (proc/opt) netlist, synthesis script and equivalence script are original outputs or commands for the same experiment.
design.sv:
// SPDX-License-Identifier: MIT
`timescale 1ns/1ps
module comparison_pair(
input wire clk, reset, a, b, select_bit, enable, data_bit,
input wire [7:0] narrow,
output reg sensitivity_bad,
output wire sensitivity_fixed,
output wire [15:0] width_bad, width_fixed,
output reg if_bad, case_bad, case_fixed,
output wire if_fixed,
output reg race_a_bad, race_b_bad,
output reg race_a_fixed, race_b_fixed,
output reg initial_only, reset_fixed
);
// Deliberately incomplete event list: b is also read.
always @(a) begin
sensitivity_bad = a ^ b;
end
assign sensitivity_fixed = a ^ b;
// The intended value is signed; the input declaration is unsigned.
assign width_bad = narrow;
assign width_fixed = $signed(narrow);
// Unknown control behavior is visible only in a four-state execution.
always @* begin
if (select_bit) if_bad = a;
else if_bad = b;
end
assign if_fixed = select_bit ? a : b;
always @* begin
casex (select_bit)
1'b0: case_bad = b;
1'b1: case_bad = a;
endcase
end
always @* begin
case (select_bit)
1'b0: case_fixed = b;
1'b1: case_fixed = a;
default: case_fixed = 1'bx;
endcase
end
// Separate processes race through blocking assignments.
always @(posedge clk) begin
if (reset) race_a_bad = 1'b0;
else race_a_bad = race_b_bad;
end
always @(posedge clk) begin
if (reset) race_b_bad = 1'b1;
else race_b_bad = race_a_bad;
end
always @(posedge clk) begin
if (reset) begin
race_a_fixed <= 1'b0;
race_b_fixed <= 1'b1;
end else begin
race_a_fixed <= race_b_fixed;
race_b_fixed <= race_a_fixed;
end
end
// An initial value is not a response to the reset input.
initial initial_only = 1'b1;
always @(posedge clk) begin
if (enable) initial_only <= data_bit;
end
always @(posedge clk) begin
if (reset) reset_fixed <= 1'b0;
else if (enable) reset_fixed <= data_bit;
end
endmodule
// One corrected expression checked against its explicit bit-level reference.
module extend_gold(input wire [7:0] value, output wire [15:0] result);
assign result = $signed(value);
endmodule
module extend_gate(input wire [7:0] value, output wire [15:0] result);
assign result = {{8{value[7]}}, value};
endmodule
tb.sv:
// SPDX-License-Identifier: MIT
`timescale 1ns/1ps
module tb;
reg clk = 0;
reg reset = 1;
reg a = 0;
reg b = 0;
reg select_bit = 0;
reg enable = 0;
reg data_bit = 0;
reg [7:0] narrow = 8'hff;
wire sensitivity_bad, sensitivity_fixed;
wire [15:0] width_bad, width_fixed;
wire if_bad, if_fixed, case_bad, case_fixed;
wire race_a_bad, race_b_bad, race_a_fixed, race_b_fixed;
wire initial_only, reset_fixed;
comparison_pair dut(.*);
initial begin
#1;
$display("WIDTH bad=%h fixed=%h", width_bad, width_fixed);
if (width_bad !== 16'h00ff || width_fixed !== 16'hffff)
$fatal(1, "declared width-extension result changed");
a = 1;
#1;
a = 0;
#1;
b = 1;
#1;
$display("SENSITIVITY bad=%b fixed=%b", sensitivity_bad, sensitivity_fixed);
if (sensitivity_fixed !== 1'b1) $fatal(1, "fixed combinational result");
a = 1;
b = 0;
select_bit = 1'bx;
#1;
$display("UNKNOWN if_bad=%b if_fixed=%b case_bad=%b case_fixed=%b",
if_bad, if_fixed, case_bad, case_fixed);
// No cross-tool unknown-value assertion: one run uses two-state policies.
clk = 1;
#1;
$display("RESET initial_only=%b reset_fixed=%b", initial_only, reset_fixed);
if (reset_fixed !== 1'b0) $fatal(1, "explicit reset did not clear state");
clk = 0;
reset = 0;
#1;
clk = 1;
#1;
$display("RACE edge=1 bad=%b%b fixed=%b%b",
race_a_bad, race_b_bad, race_a_fixed, race_b_fixed);
if ({race_a_fixed,race_b_fixed} !== 2'b10) $fatal(1, "first exchange");
clk = 0;
#1;
clk = 1;
#1;
$display("RACE edge=2 bad=%b%b fixed=%b%b",
race_a_bad, race_b_bad, race_a_fixed, race_b_fixed);
if ({race_a_fixed,race_b_fixed} !== 2'b01) $fatal(1, "second exchange");
$display("PASS: known-value checks; unknown outputs are observations only");
$finish;
end
endmodule
The source uses an explicit time unit, narrow declared widths and known-value checks that stop on an unexpected result. The unknown-control outputs are observations, not a cross-tool assertion that a two-state execution must return x. Icarus’s -g2012 and strict expression-width option are recorded explicitly; its command reference distinguishes language selection from other generation options1.
The Verilator run records --x-assign 0 and --x-initial 0. Those choices belong to the result, not an unstated assumption that all simulators represent unknown state identically. Verilator’s pinned language documentation describes its two-state model, while its option reference describes the unknown-value controls23. The Yosys output is a generic (proc/opt) netlist, not a cell-mapped netlist, with no cell-library delays or routed parasitics.
Read each mismatch in context
| Observation | Icarus RTL | Icarus generic (proc/opt) | Verilator RTL | Verilator generic (proc/opt) |
|---|---|---|---|---|
Incomplete sensitivity output after only b changes | 0 | 1 | 0 | 1 |
| Complete combinational alternative | 1 | 1 | 1 | 1 |
| Unsigned / signed extension | 00ff / ffff | 00ff / ffff | 00ff / ffff | 00ff / ffff |
Unknown if: original / alternative | 0 / x | x / x | 0 / 0 | 0 / 0 |
| Unknown case: original / alternative | 0 / x | x / x | 0 / 0 | 0 / 0 |
| Blocking race after first exchange edge | 11 | 10 | 11 | 10 |
| Nonblocking alternative after first edge | 10 | 10 | 10 | 10 |
| Initial-only / explicit reset response | 1 / 0 | 1 / 0 | 1 / 0 | 1 / 0 |
An incomplete event list retains an old answer
The first process reads both a and b but waits only for a change to a. The testbench deliberately changes a in both directions before changing only b, so the old answer is established rather than depending on startup state. The procedural RTL output then stays zero even though the expression’s current value is one. The continuous-assignment alternative follows the expression.
After Yosys converts the process into combinational logic, both Yosys generic (proc/opt) netlist outputs become one for that input. This is a useful counterexample to “the RTL simulation passed, so the logic must implement the same observed sequence.” The event list was part of the simulation’s update behavior. The generated logic has no mechanism corresponding to waiting for only that named input.
For a larger design, first check whether the supposed combinational process is actually intended to retain state. Replacing an event list does not repair a missing assignment or a deliberately clocked process. Here the expression is purely combinational, so a continuous assignment makes the requirement and dependency clear without introducing another scheduling choice.
Signedness can be consistently wrong for the requirement
The byte ff enters an unsigned eight-bit port. Assigning it directly to sixteen bits produces 00ff; casting it to signed before widening produces ffff. Every execution in the table agrees on those two results. Tool agreement therefore does not establish that the chosen expression matches the numeric interpretation required by the interface.
Trace width and signedness through the whole expression rather than looking only at the destination declaration. A wider destination cannot restore a sign that was already interpreted as unsigned. In this example the correction is intentionally narrow: the input represents a signed byte, and the cast occurs before widening. A protocol carrying an unsigned magnitude would need the original extension instead.
An unknown control is a separate question
With a equal to one, b equal to zero and the control unknown, the original RTL if follows its else branch in the four-state run. The conditional operator produces an unknown result because its alternatives differ. The casex version likewise chooses a definite branch, while the explicit case default marks an unknown control. These alternatives make the uncertainty visible in this simulation; they do not build a physical circuit that detects the abstract value x.
The generic (proc/opt) netlist and two-state observations show why an unknown-value policy must travel with a report. A generic (proc/opt) netlist transformation can treat unknown assignments as implementation freedom, and a two-state execution cannot reproduce every four-state observation. Do not use the all-zero unknown row as evidence that reset, power-up or control integrity is correct. Add the relevant reset and control requirements to separate tests.
Blocking assignments create an order-dependent exchange
Two processes are meant to exchange their previous values at a clock edge. With blocking assignments, whichever process runs first can change a value before the other reads it. The recorded RTL executions collapse both bits to one after the first exchange. That is one observed schedule, not a guarantee that every simulator or source arrangement chooses that same collapsed value.
The single nonblocking process computes both next values from the previous state. It produces 10 after the first exchange and 01 after the second. The Yosys generic (proc/opt) netlist also shows that exchange for the original pair, exposing the RTL-versus-generic-netlist mismatch. Check the intended state transition directly; matching one accidental procedural order is not a useful hardware requirement.
An initial value does not implement a reset request
The initial-only register starts at one and ignores the reset input. Its alternative clears at the clock edge while reset is asserted. All four executions preserve that distinction. The result shows that this Yosys flow retained the initial value; it does not demonstrate how a chosen implementation technology powers up.
If the system requires a reset response, write and test that response. If it instead relies on a technology-supported initialization mechanism, record that implementation contract and verify it in the relevant flow. A successful Yosys generic (proc/opt) netlist simulation supplies neither a library-specific reset circuit nor evidence of a power-on state in fabricated hardware.
Keep the equivalence obligation small and named
The separate extend_gold and extend_gate modules express the corrected signed extension in two forms: a signed cast and explicit copies of the sign bit. The Yosys script creates equivalence cells, attempts to prove them and asks equiv_status -assert to fail if any remain unproven. That final option’s documented role is to make an incomplete proof an error4. The recorded run reports success for all sixteen result bits.
This is a proof of that combinational relation under the selected flow. It is not a proof of the entire testbench, the deliberately faulty examples, timing closure or power consumption. A larger equivalence task must identify its compared designs, state mapping, reset assumptions and treatment of undefined values. The formal-results guide distinguishes a bounded result from an unbounded argument and from an inconclusive run.
Preserve rejected attempts instead of hiding them
The first naming attempt used design as a module identifier. Icarus returned design.sv:3: syntax error; Verilator returned %Error: design.sv:3:8: syntax error, unexpected design. The original files and literal diagnostics are retained with the example. The final attempt renames that identifier to comparison_pair, updates its instantiation and top selection, and adds a = 1; #1; a = 0; #1; before the b-only change. That added stimulus establishes the sensitivity-list row’s prior value; the final run is not merely an identifier rename. It neither disables the rejected syntax check nor presents the rejected source as a successful run.
Keep the source and options that produced a disagreement before experimenting with a correction. Otherwise an apparently clean result may only show that the experiment changed. For a useful handoff, pair the failing observation with the intended requirement, the smallest corrected source and the new result. Then pursue timing or physical questions in their own constrained flow, as described in the timing-constraint guide.
Not covered on this page
- Standard-cell mapping, placement, routing or delay annotation.
- Power analysis or technology-specific initialization guarantees.
- Equivalence of the entire example design.
- Exhaustive simulator conformance or scheduling behavior.
Sources
-
Icarus command-line flags, checked 24 October 2026; Icarus 13.0 used with the recorded generation flags. ↩
-
Verilator input-language documentation at the 5.052 release commit, checked 24 October 2026. ↩
-
Verilator argument documentation at the 5.052 release commit, checked 24 October 2026. ↩
-
Yosys
equiv_statusdocumentation, version 0.55 public reference checked 24 October 2026; the same command help and actual result were recorded from the Yosys 0.70+post binary used here. The official v0.70 tag points to28ba3cb926fe376fa05ebfc1b921747d011d4557; the binary reports0c11ea098be065e585a90a06e7b7da49936cf231. These are recorded separately, not claimed to be the same source revision. ↩
Code on this page is original and MIT licensed.
How we test: methodology. Found a mistake? Corrections.