# Verilator syntax support boundaries, measured: what Verilog/SystemVerilog runs and what fails silently

## The short answer

Verilator 5.052 covers far more than “synthesizable RTL”: delays, real arithmetic, open-drain pullup/nmos, tri-state and SVA work. It lacks gate-level precision; its worst failures are silent.

**How this was established**: the same source was given to Verilator 5.052 and VCS 2023.12 for A/B comparison; the support matrix comes from 39 standalone cases (one file per construct), each linted, built and run — not copied out of documentation.

## Environment and method


| item      | value                                                                                                                                   |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| simulator | Verilator 5.052 (2026-09-05), gcc 12.5.0                                                                                                |
| platform  | CentOS 7.9 / glibc 2.17, no root                                                                                                        |
| reference | VCS 2023.12-SP1                                                                                                                         |
| method    | one file per construct → `verilator --lint-only --timing -Wno-fatal` → the suspicious ones again with `--binary --timing` build + run |
| tally     | 39 cases: lint 38 PASS / 1 FAIL; the one that FAILs (`tran`) also fails to build                                                        |

## What a real design needed changed (brief)

Subject: a 1080p timing controller on a 7-series FPGA, 29 modules, 100 MHz input, 490 MHz LVDS serial output, I2C slave; the testbench runs 43 ms of simulation time.

Four changes are mandatory:


| # | change                                                               | why                                                                                    |
| - | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| 1 | `$fsdbDumpfile/$fsdbDumpvars/$fsdbSuppress` → `$dumpfile/$dumpvars` | no Verdi PLI; `$fsdb*` is an unknown system task                                       |
| 2 | unisims + secureip → behavioural fast models                        | secureip is encrypted source Verilator cannot read, and the unisim SERDES depend on it |
| 3 | instantiate `glbl glbl();` inside the testbench                      | some primitives use the XMR `glbl.GSR`, and the instance must be named exactly `glbl`  |
| 4 | waveform format → VCD or FST                                        | see “Waveform pitfalls”                                                              |

Result: the two BMP frames the TB dumps are **byte-identical** to the VCS run (same md5), and the end time agrees to 1 ps; build 4.6 s (-j32) against 23 s for VCS, simulation 52 s against 51 s.

## Syntax support matrix (measured)

### Fully supported, and identical to VCS


| category                          | constructs                                                                                                                  |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| sequential / combinational blocks | `always_ff` / `always_comb` / `always_latch` / `always @(*)`                                                                |
| composite types                   | packed struct, unpacked struct, packed union, `typedef enum` + `'( )` casts                                                 |
| packages and interfaces           | `package` + `import`, `interface` + `modport`                                                                               |
| classes                           | `class`, `new`, `virtual function` (an implicit static lifetime draws IMPLICITSTATIC)                                       |
| containers                        | dynamic array `int a[]`, queue `int q[$]`, associative array `int tbl[string]`                                              |
| strings                           | `string`, `$sformatf`                                                                                                       |
| assignment patterns               | `'{default:1'b1}` packed pattern, `'{0,1,2,3}` array pattern                                                                |
| generate                          | `generate for` + `genvar`                                                                                                   |
| assertions                        | immediate `assert(...) else $error`, concurrent `assert property` (really executed, see below)                              |
| coverage                          | `covergroup`/`coverpoint` + `--coverage` (`verilator_coverage` reports covergroup bin hit rates)                            |
| process control                   | `fork/join`, `join_any`, `join_none`, `wait fork`, `disable fork`, **`disable {named fork}`**                               |
| procedural continuous assignment  | `force` / `release`                                                                                                         |
| files and randomness              | `$readmemh/$readmemb`, `$fopen/$fwrite/$fclose`, `$urandom`, `randcase`                                                     |
| delays                            | procedural `#(0.250ns)`, continuous assignment `assign #(real variable)`                                                    |
| tri-state and gate level          | `pullup`/`pulldown`, `nmos`/`pmos`, `and/nand/or/nor/xor/buf/not`, `tri` inout                                              |
| other                             | `casez`/`casex`, `unique case`, signed arithmetic, array ports, `let`, `inside`, XMR (`u_sub.sig` read/write), DPI-C import |

Two conclusions that go against older folklore, both settled by measurement:

- **`disable {named fork}` is supported.** `fork: W ... join_any; disable W;` leaves only the winning branch printing; `disable fork` works as well.
- **SVA is not merely parsed.** On a violation it really prints `%Error: Assertion failed ... %Error: Verilog $stop` and **aborts the simulation** — treat that as a failure in CI.

### Not supported (hard error)


| construct                                                      | error                                                                                |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `tran` / `tranif0` / `tranif1` bidirectional switches          | `%Error-UNSUPPORTED: Verilog 1995 gate primitive: tranif1`, the build fails outright |
| encrypted vendor models (secureip and friends)                 | the source cannot be read, so it cannot be parsed                                    |
| `$fsdb*` and other Verdi-only system tasks                     | no matching PLI, unknown system task                                                 |
| gate-level / SDF back-annotation, `$setup/$hold` timing checks | no equivalent implementation                                                         |

### Accepted but silently wrong (the class to watch)


| construct                                                   | Verilator                                                                            | VCS                  | note                                                                          |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------- |
| drive strength: `pullup` against `assign (weak0,weak1) s=0` | `0`                                                                                  | `1`                  | **no warning**; any two drivers are resolved by value, strength plays no part |
| `specify ... endspecify` path delay                         | y toggles at 2000 ps                                                                 | y toggles at 3000 ps | only a `%Warning-SPECIFYIGN`, the delay is dropped                            |
| `$dumpoff` / `$dumpon`                                      | parsed but **has no effect**                                                         | works                | the dump keeps writing to the end of the run                                  |
| `--no-timing` or `+notimingchecks`                          | every `#delay` becomes `%Warning-STMTDLY: Ignoring delay`, all edges collapse to t=0 | —                   | with `-Wno-fatal` this **silently changes behaviour**                         |

On `--timing`: **5.x has it on by default**, so not passing it gives the same result; what you must guard against is turning it off (or `+notimingchecks`, which is “accepted and ignored”). Write `--timing` explicitly in the script anyway, so the intent stays on the command line.

## Waveform pitfalls

1. **`$dumpvars` needs `--trace` (VCD) or `--trace-fst` (FST) at build time.** Without that compile-time switch `$dump*` is a silent no-op: the run finishes with no file and no message.
2. **`$dumpoff/$dumpon` do nothing** (see the table above), so “dump only one window” is impossible; the only knob is the level argument of `$dumpvars(level, scope)`.
3. **Three orders of magnitude in size**: for the same 43 ms design with two 490 MHz buses, dumping only the TB and the DUT top level — VCD **12.4 GB**, FST **123 MB**. There is no equivalent of Verdi's suppress, so the only levers are scope level or FST.
4. **Dumping is itself the main cost**: same configuration, 52 s with no waveform, 152 s with FST (about 3x), about 98 s with VCD; on the VCS side enabling FSDB adds 0 s. If you want speed, do not dump by default.
5. **Verdi cannot read FST**, GTKWave reads it natively; building FST support also needs the lz4 headers (`fst2vcd` converts to VCD for other tools).
6. **`$time` returns an integer number of time units**: in a 1 ns/1 ps design a `%0t` print shows `42922750.000 ns`, which looks 355 ps worse than VCS, while the last timestamp in the VCD is `42922749645 ps` — the two agree. Judge the time axis from the waveform timestamps, not from the `$time` print.

## Rules when writing TB / RTL (checklist)

1. **Do not start a comment with `// Verilator`.** Verilator treats `// verilator ...` as a pragma and fails with `%Error-BADVLTPRAGMA: Unknown verilator comment`. Use a prefix such as `// NOTE:` or `// VLNOTE:`.
2. **Check whether a behavioural model of the primitive exists.** An IP available only as encrypted secureip is a dead end; where a fast behavioural model exists, functional simulation is fully adequate.
3. **An XMR into a global (such as `glbl.GSR`) needs the right instance name**: write `glbl glbl();` in the testbench (instance named like the module); Verilator resolves it by searching upward.
4. **`-Wno-fatal` must come with warning triage.** Grep the `UNSUPPORTED` / `SPECIFYIGN` / `STMTDLY` / `WIDTHEXPAND` / `MULTIDRIVEN` codes out and judge each one; using `-Wno-fatal` as “zero warnings” lets the silent distortions through.
5. **A failing assertion `$stop`s the simulation**, so a regression script must treat it as a failure in both exit code and log matching, not as “the run finished”.
6. **Do not rely on drive strength or gate delays.** The common open-drain plus pull-up topology (“strong0 overrides pull1”) comes out right, but “pull1 overrides weak0” is wrong, and there is no warning.
7. **Run `--lint-only --timing` before building**: clearing the syntax problems in the lint stage is much faster than trying again with a 4 s compile each time.
8. **Give the dump only the levels you need**: `$dumpvars(1, tb)` plus `$dumpvars(1, dut)` is usually enough to locate a problem; dumping the whole hierarchy costs an unreasonable amount of size and time.

## When not to use Verilator


| scenario                                                 | reason                                                                                                    |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| gate-level / post-layout simulation, SDF back-annotation | no specify/gate delays, no timing checks                                                                  |
| models that depend on drive strength                     | strength does not take part in resolution, silently wrong                                                 |
| IP available only as an encrypted model                  | the source cannot be read                                                                                 |
| a flow tied to Verdi (`$fsdb*`, KDB)                     | no PLI; waveforms only as FST/VCD                                                                         |
| expecting a large simulation speed-up                    | here 52 s against 51 s: 5x faster to build, no faster to run (clock edges dominate, not event scheduling) |

## How to reproduce

One case per construct, one file each, lint first and build second:

```bash
# a single construct
verilator --lint-only --timing -Wno-fatal --top-module m case.sv
# the suspicious ones (assertions / coverage / process control) really run
verilator --binary --timing -Wno-fatal --coverage --top-module m -o V case.sv && ./obj_dir/V
# coverage summary (reports covergroup hit rates)
verilator_coverage coverage.dat
```

To check “does it agree with the commercial simulator”, give the same file to both simulators and compare the printed times and values (every “identical / different” in this note comes from doing exactly that).

## Limitations

The data here comes from a single Verilator version (5.052), 39 syntax cases and one real design of 29 modules, so it does not describe how earlier or later versions behave; `program`, `clocking` blocks and DPI-C were only proven to parse/build, their runtime semantics were not checked case by case. Re-run the lint matrix after upgrading Verilator.

