Migrating to v2

What changed, why, and what to do about it.

Most projects need no changes. Backward compatibility in v2 is interface-level and non-negotiable: every v1 design, cocotb testbench, ryusim CLI invocation, and Makefile.ryusim flow runs and passes on v2, and code-coverage results are equivalent.

What is not promised is bit-for-bit waveform identity and event-ordering identity. v2 replaces the v1 settle loop with a full IEEE 1800-2023 stratified region scheduler, so the order in which independent, unordered events resolve can differ. The VPI callback contract that cocotb depends on is preserved exactly.

Ordering: what to do if something moves

If a testbench passes on v1 and fails on v2 in a way that traces back to event order, the testbench is depending on an ordering the language does not guarantee. v2 does not offer a "v1 ordering" mode — the fix is to make the dependency explicit:

Behavior deltas at a glance

Change Opt-out / control
fork blocks now execute (v1 warned and dropped them) none — this is the LRM behavior
Assertions now evaluate (v1 ignored immediate and concurrent assertions) --assertions=off
Uninitialized 4-state variables read X --no-x-init, or RYUSIM_ZERO_INIT set to anything but 0/false
Unimplemented constructs are compile errors (v1 warned and dropped) --unsupported=warn
Design-side $display/$monitor output now prints none — remove the calls if you do not want the output
All top-level modules elaborate; --top selects the primary root without suppressing siblings pass only the sources you want elaborated
VCD/FST headers carry the design's timescale (v1 hardcoded 1 ns) none — regenerate golden waveforms
Coverage output is now written on the cocotb/VPI path (v1 wrote none) none — omit the coverage flags to suppress
Coverage files gained covergroup and four-outcome assertion records records appear only when the matching coverage flag is passed
Generated simulation binaries are built with a host-probed -march RYUSIM_MARCH=x86-64 (or off)
--gpu is a no-op that warns drop the flag

Simulation semantics

fork blocks now execute

In v1 any design containing a fork was diagnosed as non-synthesizable and the fork body was dropped — the forked branches never ran. v2 implements IEEE 1800-2023 §9.3.2 fork/ join/join_any/join_none, including nested forks, deferred child start, per-spawn automatic-variable capture, and disable fork/wait fork.

What to do: review any fork block that was previously inert — its branches now take effect. There is no flag to restore the v1 warn-and-drop behavior.

Assertions now evaluate

v1 parsed both immediate and concurrent assertions and then ignored them. v2 implements clause 16: sequences, the property algebra, multiclock, disable iff, sampled-value functions, sequence local variables, recursive properties, expect, let, and the assertion control tasks. An assert property that legitimately fails on your stimulus now reports (an $error-class message naming the assertion and its attempt start tick; simulation continues past the failure unless the assertion's own action block says otherwise).

Related: an assert property whose sampling clock had no other consumer in the design used to be silently eliminated along with its clock generator, so it never evaluated at all. It now evaluates — assertions on a free-running clock that nothing else observes will fire where they legitimately fail.

What to do: fix the assertion or the stimulus. If you need a staged migration, compile with --assertions=off (equivalent to $assertoff at time 0: concurrent-assertion evaluation is suppressed and coverage records the attempts as DISABLED); a runtime $asserton re-enables, and $asserton/$assertoff/$assertkill/ $assertcontrol give per-scope control.

Per LRM 16.3, checking an immediate assume as if it were an assert is an optional capability, so RyuSim does not do it by default; pass --assume-check to opt in. Concurrent assume property always checks.

Uninitialized 4-state variables read X

v1 zero-initialized variables at construction. v2 applies the IEEE 1800-2023 rule by default: an uninitialized 4-state variable (reg/logic/integer, with no declaration initializer and no time-zero write) reads X.

What to do: reset your design before sampling, which is what the standard expects. X-initialization also surfaces designs that never converge from an unknown state — if a simulation that used to terminate now runs away, the missing reset is the bug. To stage the migration, compile with --no-x-init, or set RYUSIM_ZERO_INIT to any value other than 0/false, to restore the v1 zero-init emission.

Unimplemented constructs are compile errors

v1 had two decoupled paths for constructs it did not implement: a diagnostic that warned, and a converter that silently dropped whatever it did not handle. v2 unifies both into one gate that fails the compile by default, so an unimplemented construct can never mis-simulate silently.

What to do: nothing, for almost every project — v2 implements the constructs v1 rejected. If you hit the gate during triage, --unsupported=warn restores diagnose-and-continue.

Design-side output now prints

In v1, $display, $write, $monitor, and $strobe were dropped when the design was lowered, so design-side output was observable only on the VPI/cocotb path. v2 executes them in the design, so a standalone simulation prints them and the text appears alongside your testbench's log output.

What to do: if a script parses simulator output, expect additional lines. Nothing else changes.

All top-level modules elaborate

Per LRM 23.10.3 every module that is not instantiated by another module is an elaboration root. v1 simulated only the module named by --top; v2 elaborates every root, and --top now selects the primary root (the one that drives VPI naming, the cocotb toplevel, and the binary name) without suppressing the others. Single-root designs — which is nearly every cocotb, validation, and production flow — are unaffected.

What to do: if you relied on --top to prune sibling top-level modules out of the simulation, pass only the sources that contain the roots you want elaborated.

Output artifacts

Waveform timescales are design-true

v1 wrote a hardcoded 1 ns timescale into every VCD and FST header regardless of the design's `timescale directive. v2 writes the design's own time unit and precision, so headers change for any design that carries a timescale directive other than 1 ns, and timestamps in the file are expressed in that unit.

What to do: regenerate any golden waveform captured on v1, and update tooling that assumes nanosecond ticks. See the Waveform Guide.

Coverage output

On v1, a cocotb run produced no coverage.info at all — coverage was flushed only from the standalone simulation's main(), which the VPI event loop exits before reaching. v2 writes the file on both paths, and writes a zeroed template at time 0 so the file exists mid-run. If your cocotb flow tolerated a missing coverage file, or read a stale one left over from a standalone run, it now gets fresh output every run.

Where both versions produced a file, results are equivalent: the same lines and branches are covered, and the same signals are toggle-covered. Hit counts may differ, because the v2 scheduler legitimately evaluates a different number of times.

The LCOV file gained records for capabilities v1 did not have:

These records were labelled "subject to change until GA" during the v2 alphas. That caveat is now resolved: the formats above are the GA formats. Both are RyuSim extensions to LCOV, like the existing TGDA toggle records; strict lcov 2.x needs --ignore-errors format to read them, and ryusim coverage merge/ryusim coverage html handle them natively. See the Coverage Guide.

The obj_dir layout is not a contract

obj_dir holds generated C++ and build scaffolding. Its file names, file count, and internal structure are implementation detail and changed substantially in v2 — they will keep changing across releases. Only the CLI, the simulation binary it produces, and the artifacts you ask for by flag (waveforms, coverage files) are stable surfaces.

What to do: do not build tooling that reads or patches files inside obj_dir.

Generated code is built for the host CPU

v1 passed no -march when compiling the generated simulation. v2 probes the build host's psABI level and compiles the generated code for it, which means a simulation binary built on one machine is not guaranteed to run on an older one.

What to do: if you copy obj_dir binaries between machines, set RYUSIM_MARCH — accepted values are off (emit no -march), native, x86-64, x86-64-v2, x86-64-v3, and x86-64-v4. x86-64 is the portable baseline.

Command line

Every documented v1 invocation form still works. The changes are:

The full list is in the CLI Reference.

Installing v2

The installer defaults to the stable channel, so a plain install gets the current v2 release:

curl -fsSL https://ryusim.com/install.sh | bash

v1 support

v2 is the current release line. The v1 maintenance line (release/1.x) is no longer developed — v1.7.66 is its most recent release — and a formal sunset was announced with the v2.0.0-alpha.1 prerelease. That sunset has not been executed: the branch is not locked, and no end date has been set.

New fixes land on v2 only. If something that worked on v1 does not work on v2 and it is not one of the deltas above, that is a bug — please report it.