VCS bug: {N{s}} == const gives 0 instead of x when s is x or z (incl. out-of-range bit-select)
Synopsys VCS folds an equality between a replicated 1-bit signal and a constant to 0 when the constant isn't all-0s or all-1s. That is valid in 2-state logic (N copies of one bit can never equal 3'b001), but wrong in 4-state: IEEE 1800's == gives x when x/z bits make the result ambiguous, and 0 only when some known bit pair differs. Icarus 12.0 and slang 12.1 give x.
Minimal repro, one statement, default flags:
module m;
logic s2 = 1'bz;
initial begin #1; $display("%b", {3{s2}} == 3'b001); end
endmodulevcs -full64 -sverilog prints 0 on every version tried: O-2018.09-SP2-3, R-2020.12-SP1-1, U-2023.03-SP2, U-2023.03-SP2-2-T and W-2024.09. {s2,s2,s2} == 3'b001 gives 0 on 2020 to 2024 with default flags. r3 == 3'b001 with r3 = 3'bzzz (no replication visible) is correctly x everywhere.
The out-of-range variant: with logic [4:3] s1 = 2'b01;, s1[2] is out of range, so it reads as 1'bx (VCS warns SIOB, and prints x for s1[2] itself). {3{s1[2]}} == 1'sb1 should be x. VCS gives:
- 0 on O-2018.09 with default flags;
- 0 on every version with -debug_access+all;
- x when alone on 2020 to 2024 with default flags. It gives 0 again when the module also has certain unrelated $display statements; a 4-statement repro was found by delta debugging, and removing any one of the three unrelated lines makes it correct.
As with the reduction case, the exact text matters, which points to a compile-time optimization (copies of one bit treated as one shared unknown) rather than runtime evaluation. -xprop=tmerge/xmerge, -O0, -Xnoopt and -lca don't change it; +rad happens to make the s1[2] form correct but not the s2 form.
Practical impact: an if (bus == PATTERN) built from a replicated unknown bit takes the false branch instead of being x, which can hide X bugs in RTL that VCS simulates. Check suspicious cases with === or $isunknown() first, or confirm with a second simulator.
Update after a much larger re-test: 208 single-test files, run in 307 file/mode combinations on each of three VCS releases (921 runs). VCS returns 0 instead of x for == on replicated unknown bits. Whether it happens depends on how the x was created, on unrelated declarations nearby, and on whether wave dumping or debug access is on. This replaces my earlier write-up, which was accurate for the files it tested but too broad in places (corrections at the end).
Background. Four-state simulation uses 0, 1, x (unknown) and z (high impedance). IEEE 1800-2017 §11.4.5: == and != return x when x/z bits make the result ambiguous; == returns 0 only if a pair of known bits differs. So with
en= x,{4{en}} == 4'b0101must be x. Icarus Verilog 12.0 gives x, and slang 12.1's constant evaluation gives x for the same expressions.Smallest repro. VCS R-2020.12-SP1-1, U-2023.03-SP2 and W-2024.09,
vcs -full64 -sverilog, with default flags or-debug_access+all:module tb; logic en = 1'bx; initial #1 $display("eq %b ne %b", {4{en}} == 4'b0101, {4{en}} != 4'b0101); endmoduleVCS prints
eq 0 ne x; Icarus printseq x ne x. VCS returns false for equality but unknown for inequality on the same operands. Both should be unknown.Same value, same run, two answers (
logic en;never assigned, nothing else in the module):$display("%b", {en, en, en, en} == 4'b0101); // VCS: x $display("%b", {4{en}} == 4'b0101); // VCS: 0It reaches logic, not just $display. In one run:
logic en = 1'bx; // e.g. an unreset control bit wire [7:0] y = ({4{en}} == 4'b0101) ? 8'hAA : 8'h55; always @(posedge clk) hit_q <= ({4{en}} == 4'b0101);Default flags:
y= xx (correct) buthit_q= 0. With-debug_access+all:y= 55 (the false branch) andhit_q= 0. Icarus gives xx and x.Dumping waves changes the answer in both directions. W-2024.09, identical matrix files (with the two skeleton declarations described in the next comment, a
$displayconsumer and no extra context); the only change is enabling$dumpfile/$dumpvars:{en,en,en,en} == 4'b0101, en = 1'bx default 0 with $dumpvars x (dump hides the bug) {4{en}} == 4'b0101, en never assigned default x with $dumpvars 0 (dump causes the bug) {4{v[2]}} == 4'b0101, v[2] out of range default x with $dumpvars 0What the full matrix shows, within the tested combinations (details in the next comment):
- Every wrong result was 0, or the 0 / false branch downstream. None of the 43 control combinations, where the correct answer really is 0, came out wrong on any release.
- Affected: == and ==? on a replicated 1-bit unknown (
{2{en}},{4{en}},{8{en}},{{2{en}}, 2'b01}, a replicated out-of-range bit-select), and the written-out{en,en,en,en}whenenwas initialized to, or assigned, x/z. Never wrong in our files: !=, ===, and a replicated 2-bit vector.- How the x was created matters. For
{4{en}} == 4'b0101with a$displayconsumer and no extra context: ifenwas initialized to 1'bx or 1'bz, or assigned x at time 0, the result is wrong with default flags. Ifenwas never assigned, it is wrong in a bare module, correct once two unrelated declarations are added, and wrong again with$dumpvarsor-debug_access+all.- With
$dumpvars, a ternary and$isunknown()on{4{en}} == 4'b0101still gave xx / 1, while a variable, wire, flop or case on the same comparison got 0. Under-debug_access+all(uninitializeden, no extra context), every tested consumer of that comparison behaved as if it were 0.- All 307 results were identical across the three releases. 55 default-mode combinations per release were also rerun in fresh directories, and all 165 matched. Repeatable, but sensitive to code and flags that shouldn't matter.
Corrections to my earlier comments:
- I said printing, a wire, a variable or a function "made no difference". That held for the earlier 19-file set, but not in general.
- I said if/ternary uses were correct. A ternary is wrong under
-debug_access+all.- The R/C/O table was accurate for those exact files. Read it as one slice, not the rule.
Practical advice. Don't rely on VCS == against replicated unknown bits. Check the inputs with === or $isunknown, cross-check X behaviour on a second 4-state simulator, and if a result changes when you add waves or debug access, suspect the simulator as well as the design.
Limits. This is an observed behaviour on three installed releases on one Linux host that VCS reports as unsupported, not a vendor-confirmed diagnosis. Nothing newer than W-2024.09 was available, so 2025+ may be fixed. I haven't been able to search Synopsys SolvNet, so it may already be known.
Methods for the re-test: what was varied, what was held fixed, and the raw counts.
Tools
- VCS R-2020.12-SP1-1, U-2023.03-SP2, W-2024.09.
vcs -full64 -sverilog, run as three modes: default,+define+DUMP(adds$dumpfile/$dumpvars(0, tb)to the same file), and-debug_access+all.- Reference: Icarus Verilog 12.0 (
iverilog -g2012), checked against expected values derived from IEEE 1800-2017 §§11.4.5-11.4.6 (equality, wildcard equality), 11.4.11 (conditional operator), 11.4.12 (concatenation/replication), 12.5 (case) and 20.9 ($isunknown). slang 12.1 constant evaluation gives the same values for the key expressions.- Verilator is 2-state for this, so it can't referee it.
Design of the matrix. One test per file, because surrounding code changes the answer. Every file has the same skeleton (
logic [1:0] en2; logic [4:3] v = 2'b01;, the x source, optional context, one consumer, one print), varied along five factors:Expression (11) {4{en}}==4'b0101, {en,en,en,en}==4'b0101, {2{en}}==2'b01, {8{en}}==8'h5A, {4{en}}!=4'b0101, {{2{en}},2'b01}==4'b0101, {2{en2}}==4'b0101, {4{en}}==?4'b0101, {4{v[2]}}==4'b0101 (out of range) controls (IEEE says 0): {{2{en}},2'b00}==4'b0101, {4{en}}===4'b0101 x source (5) logic en; | logic en = 1'bx; | logic en = 1'bz; | en = 1'bx at t=0 | wire en; (undriven) Context (7) none | unused wire [3:0] cw = {4{en}} | unused wire cw = en | a = {4{en}} before the test | $display(en) | unrelated var + $display | sibling compare Consumer (7) $display | variable | continuous wire | ternary 2'b01:2'b10 | case 0/1/default | flop q <= e | $isunknown(e) Mode (3) default | $dumpvars | -debug_access+allCombinations: expression x source (55), core expressions x context (42), core expressions + one control x consumer x mode (147), plus 80 random combinations of all five factors with seed 1. After de-duplication that is 208 distinct source files, run in 307 file/mode combinations per release, 921 runs in total, zero compile failures. A follow-up ran the 55 expression x source files with $dumpvars and with -debug_access+all on W-2024.09 (110 more runs).
Results (W-2024.09; R-2020.12 and U-2023.03 matched it on all 307):
wrong / total 83 / 307 by mode default 22/160 $dumpvars 22/76 -debug_access+all 39/71 controls 0 wrong wrong values seen 0 (67), case arm ZERO (11), ternary 10 = false branch (5) repeatability all 307 identical across the three releases; 55 default-mode combinations per release rerun in fresh directories, 165/165 identical Icarus vs IEEE 235/236 (the miss is Icarus: $isunknown of a comparison that is 0 returned 1 under $dumpvars)Uninitialized en, no extra context, by consumer and mode (! = wrong):
default $dumpvars -debug_access+all {4{en}} disp/var/wire/flop x 0 ! 0 ! {4{en}} case X ZERO ! ZERO ! {4{en}} ternary xx xx 10 ! {4{en}} $isunknown 1 1 0 ! {4{v[2]}} disp/var/wire/flop x 0 ! 0 ! {4{v[2]}} ternary/$isunknown xx/1 xx/1 xx/1 {4{en}}!= all consumers correct in all modes {en,en,en,en} all consumers correct in all modesx source with default flags, $display consumer, no context:
uninit =1'bx =1'bz x at t=0 undriven wire {4{en}} x 0 0 0 x {2{en}}, {8{en}} x 0 0 0 x {en,en,en,en} x 0 0 0 x {4{en}} ==? x 0 0 0 x {4{en}} != x x x x xSame files with $dumpvars / -debug_access+all (W-2024.09, follow-up run). Each cell is default -> dump -> debug:
uninit =1'bx =1'bz x at t=0 undriven wire {4{en}} x 0 0 0 0 0 0 0 0 0 0 0 x 0 0 {en,en,en,en} x x x 0 x x 0 x x 0 x x x x x {4{v[2]}} (oob) x 0 0 x 0 0 x 0 0 x 0 0 x 0 0 {{2{en}},2'b01} x x 0 x x 0 x x 0 0 0 0 x x 0 {4{en}} != x x x (correct in every source and mode)So dumping moves results both ways:
{en,en,en,en}with an x-initializedengoes from wrong to correct, and{4{en}}with a never-assignedengoes from correct to wrong.The bare-module repros in the comment above have no skeleton declarations. There, a never-assigned
enis already wrong with default flags. Addinga = {4{en}};or an unusedwire [3:0] w = {4{en}};before the test made it correct. That is why every matrix file uses an identical skeleton, and why the repro files should be copied exactly.Exact repro files used in the comment above (all identical on the three releases, default and -debug_access+all):
r1 logic en; $display({4{en}} == 4'b0101) -> VCS 0 Icarus x r2 logic en = 1'bx; same -> VCS 0 Icarus x r3 logic en; {en,en,en,en}==..., then {4{en}}==... -> VCS x, 0 Icarus x, x r4 logic en = 1'bx; mux + flop on the compare -> VCS xx/0 (debug: 55/0) Icarus xx/x r5 logic en = 1'bx; == and != on the same operands -> VCS 0, x Icarus x, xCaveats. This is an observed behaviour, not a vendor-confirmed diagnosis. One Linux host (a version VCS reports as unsupported), three installed releases, nothing newer than W-2024.09. "Affected" and "never wrong" refer only to the tested combinations. We don't know what inside VCS triggers the context effect. The random 80-file sample covers the full five-factor space only thinly.
Plain-language summary, for readers outside chip verification:
I observed what looks like a bug in Synopsys VCS, one of the most widely used commercial chip simulators, on three releases (2020.12, 2023.03, 2024.09).
Background: chip simulation uses four values per bit: 0, 1, x (unknown, for example a register nobody has reset yet) and z (not driven). The SystemVerilog standard says that if unknown bits make a comparison ambiguous, the answer is x, not "false". That's how unknowns stay visible during verification instead of being quietly hidden.
The whole test:
module tb; logic en = 1'bx; initial #1 $display("eq %b ne %b", {4{en}} == 4'b0101, {4{en}} != 4'b0101); endmodule{4{en}}just means "en repeated four times". VCS printseq 0 ne x: it returns false for "equal" but unknown for "not equal" on the same values. The standard says both should be unknown; Icarus Verilog agrees, and so does slang's constant evaluation for these expressions.It isn't only a printing problem. When this comparison feeds a flip-flop, VCS stores 0. With full debug access turned on, a mux driven by it takes the "false" path. In those tested cases the expected unknown is lost.
The strange part is what makes it show up. Across 208 one-test files (307 file and mode combinations per release), the answer depended on how the unknown was created, on unrelated declarations nearby, and on whether waveform dumping or debug access was on. Turning on waves, the usual first debugging step, made a correct result wrong in some files and a wrong result correct in others. All 307 primary results matched across the three releases, and 55 default-mode combinations per release also matched on fresh-directory reruns. It's repeatable, just sensitive to things that shouldn't matter.
If you verify RTL on VCS: don't rely on == against repeated unknown bits, cross-check X behaviour on a second 4-state simulator, and if a result changes when you add waves, suspect the simulator too.
Tests used one Linux host that VCS reports as unsupported. Not tested on 2025+ releases, so it may already be fixed there. I haven't been able to search Synopsys' support database, so it may already be known.