Initialization and 2-state

How RyuSim initializes variables (X or zero), what 2-state mode changes, and why results differ.

A SystemVerilog variable with no initializer and no reset has to start somewhere. The standard says where, and RyuSim follows it by default. Other simulators start in other places, so the same design can print different results. This page covers the default, the options that change it, the --2state mode, and what to expect when the results disagree.

The IEEE default

IEEE 1800-2023 clause 6.8, Table 6-7, gives the start value of a variable declared without an initializer. RyuSim uses these values unless you pass one of the options below.

Declared typeStart valueExamples
4-state integralall Xlogic, reg, integer, time
2-state integral0bit, byte, int, longint
real, shortreal0.0
string""
class handlenull

A net is not a variable. An undriven wire reads Z.

X is not a value the hardware holds. It records that the simulator does not know the value. An X spreads through arithmetic and most operators, so a register that is never reset stays X for as long as it feeds on itself.

A design that shows the difference

This counter has no reset. count is a 4-state logic, count2 is a 2-state bit, and mem is a 4-state memory that nothing writes.

module init_demo; logic [3:0] count; // 4-state, no initializer, no reset bit [3:0] count2; // 2-state, no initializer logic [7:0] mem [0:3]; // 4-state memory, never written logic clk = 0; always #5 clk = ~clk; always @(posedge clk) begin count <= count + 1; count2 <= count2 + 1; end initial begin repeat (3) @(posedge clk); #1; $display("count=%0d count2=%0d mem[0]=%0d", count, count2, mem[0]); if (count == 3) $display("count reached 3"); else $display("count did not reach 3"); $finish; end endmodule

Compile and run it with no options:

ryusim -q compile init_demo.sv --top init_demo -o x_init ./x_init/build/init_demo_sim
RyuSim - Simulation of init_demo RyuSim: effective root seed = 676780200730893571 (source: randomly generated) count=x count2=3 mem[0]=x count did not reach 3 Simulation complete. Time: 26

count starts X, and X + 1 is X, so it is still X after three clocks. count2 starts at 0 and counts to 3. mem[0] was never written, so it reads X.

Now compile the same file with every uninitialized variable and memory starting at zero:

ryusim -q compile init_demo.sv --top init_demo --init-reg zero --init-mem zero -o zero_init ./zero_init/build/init_demo_sim
count=3 count2=3 mem[0]=0 count reached 3

Why the results differ

The two runs above disagree on a value and on a branch.

Neither result is wrong for the options it was compiled with. The X result reports a real fact about the RTL: nothing sets count before it is used. Zero-init hides that. If a design passes only under zero-init, look for a register or memory that is read before reset or before its first write. The usual fix is a reset, or an initializer if the target technology supports one.

To test whether a result depends on start state, compile with --init-reg random --init-mem random and run with two seeds (see Random start values). A result that changes with the seed reads uninitialized state.

Start-value options

These options belong to ryusim compile. They apply at compile time: they choose the start values written into the generated model. To change the policy, recompile. Rerunning an existing build does not change it.

OptionDefaultApplies to
--init-reg <x|zero|one|random>xUninitialized static 4-state variables: scalars, packed vectors, packed structs.
--init-mem <x|zero|one|random>xUninitialized static 4-state unpacked arrays and memories.
--no-x-initoffSame as --init-reg zero --init-mem zero. Kept for existing scripts.
--2state (alias --two-state)offTwo-state mode; see below. Sets both start values to zero unless --init-reg or --init-mem says one or random.

The values:

The two buckets are independent. With only --init-reg zero, the counter starts at 0 but the memory still starts X:

ryusim -q compile init_demo.sv --top init_demo --init-reg zero -o reg_zero ./reg_zero/build/init_demo_sim

For each bucket, the first rule that applies wins:

  1. An explicit --init-reg or --init-mem.
  2. --no-x-init: zero.
  3. --2state: zero.
  4. Otherwise x.

Contradictory combinations stop the compile instead of picking a winner. --no-x-init or --2state together with an explicit x value is an error:

ryusim compile init_demo.sv --top init_demo --no-x-init --init-mem x 2>&1 || true

The build records the policy it used. ryusim-route.json in the output directory has an init_reg and an init_mem field:

grep -E '"init_(reg|mem)"' reg_zero/ryusim-route.json
"init_reg": "zero", "init_mem": "x",

What the options do not change

Random start values

With random, the start bits depend only on the root seed and the variable. The simulation prints the seed it used on its second line. Pass +ryusim_seed=N or set RANDOM_SEED=N to repeat a run exactly:

ryusim -q compile init_demo.sv --top init_demo --init-reg random --init-mem random -o rand_init ./rand_init/build/init_demo_sim +ryusim_seed=1 ./rand_init/build/init_demo_sim +ryusim_seed=2
RyuSim - Simulation of init_demo RyuSim: effective root seed = 1 (source: +ryusim_seed) count=2 count2=3 mem[0]=226 count did not reach 3 Simulation complete. Time: 26 RyuSim - Simulation of init_demo RyuSim: effective root seed = 2 (source: +ryusim_seed) count=3 count2=3 mem[0]=174 count reached 3 Simulation complete. Time: 26

Seed 1 and seed 2 take different branches: the design reads its start state.

Modules that ignore the policy

RyuSim has two C++ code generators. A few constructs, such as an interface that contains an always procedure, still go to the older one, which always starts storage at zero. RyuSim says so on stderr when that overrides the requested policy:

interface pass_if(input logic clk_in); logic clk; always_comb clk = clk_in; endinterface module iface_demo; logic clk = 1'b0; always #5 clk = ~clk; pass_if u_if(.clk_in(clk)); int n = 0; always @(posedge clk) begin n <= n + 1; if (n == 3) $finish; end endmodule
ryusim -q compile iface_demo.sv --top iface_demo -o iface_x 2>&1
Warning: the legacy emitter zero-initializes all uninitialized storage — the requested init policies (reg=x, mem=x) are not honored for 1 legacy-routed module(s); IEEE X-init reads 0 there Note: trigger schedule unavailable — a mixed route (1 legacy-routed module(s), e.g. 'pass_if'); this build runs the legacy-fixpoint adapter (route manifest "schedule": "legacy-fixpoint")

With --init-reg zero --init-mem zero there is nothing to override, and the warning does not appear.

VPI writes under zero-init

When both buckets are zero (from the options, --no-x-init or --2state), a VPI write that carries X or Z bits is collapsed to 0/1 before it reaches the design. This matches Verilator, which has no X to store. A cocotb driver that parks a bus at X then drives 0/1 values instead. Outside --2state, the collapse prints no message. For cocotb setup, see cocotb.

Matching Verilator

Verilator is a two-state simulator. With its default options it starts init_demo at zero. Verilator 5.038, built with verilator --binary --timing init_demo.sv --top-module init_demo, prints the same lines as the zero-init run above:

count=3 count2=3 mem[0]=0 count reached 3

RyuSim has two ways to match that start state:

OptionsStart stateRest of the simulation
--init-reg zero --init-mem zeroZero, like VerilatorStill 4-state. X and Z from literals, undriven nets, division by zero and out-of-range reads behave as the IEEE standard says.
--2stateZero, like VerilatorTwo-state, with Verilator's rules for X and Z. See below.

Use the first when only the start state differs from Verilator. Use --2state when the design or testbench also depends on Verilator's handling of X and Z.

Two-state mode (--2state)

--2state builds the whole design as a two-state model. Every integral signal holds 0 or 1. The rules follow Verilator's defaults, which is a deliberate departure from IEEE 1800-2023. Without the flag nothing below applies.

ConstructDefault (IEEE)--2state
Uninitialized 4-state variable (6.8)X0, or the explicit --init-reg/--init-mem value (one, random)
X or Z bits in a literal (5.7.1)X or Z0. In casez/casex/case inside labels they are still wildcards.
Undriven net (6.6)Z0
===, !== (11.4.5)Compare X and Z bitsCompare as ==, !=
a / 0, a % 0 (11.4.3)All X0
$isunknown (20.9)1 if any bit is X or Z0
VPI or DPI write with X/Z bitsStored as writtenCollapsed to 0/1, with one message per signal

This design exercises several rows:

module two_state_demo; logic [3:0] a = 4'b10x1; // literal with an X bit wire [3:0] w; // undriven net logic [3:0] n = 4'd9, d = 4'd0; initial begin #1; $display("a=%b w=%b", a, w); $display("isunknown(a)=%0d", $isunknown(a)); $display("n/d=%b n%%d=%b", n / d, n % d); end endmodule
ryusim -q compile two_state_demo.sv --top two_state_demo -o ts_default 2>/dev/null ./ts_default/build/two_state_demo_sim
a=10x1 w=zzzz isunknown(a)=1 n/d=xxxx n%d=xxxx
ryusim -q compile two_state_demo.sv --top two_state_demo --2state -o ts_2state 2>/dev/null ./ts_2state/build/two_state_demo_sim
a=1001 w=0000 isunknown(a)=0 n/d=0000 n%d=0000

init_demo under --2state prints the zero-init result:

ryusim -q compile init_demo.sv --top init_demo --2state -o init_2state ./init_2state/build/init_demo_sim

Limits and errors

The iface_demo.sv design from above shows the refusal:

ryusim -q compile iface_demo.sv --top iface_demo --2state -o iface_2state 2>&1 || true
ryusim compile init_demo.sv --top init_demo --2state --all-four-state 2>&1 || true

A --2state build records "two_state": true in ryusim-route.json. Every option on this page is listed in the CLI reference.