Skip to content

C. SystemVerilog reference

This page is for looking things up. It covers the part of SystemVerilog that the book's designs use, which is small on purpose, and it does not teach: each construct is explained where a chapter introduces it, and the entries below say where.

Every listing is a real file that the book's tools process (compile, lint, synthesize or simulate, depending on what the listing is for), a listing that is meant to fail says so, and every message shown is what a tool printed. The tools are slang, Verilator, Yosys and Icarus Verilog. The repository also keeps an acceptance matrix (hdl/_infra/matrix/matrix.yaml), with one small file for each construct and the recorded outcome in each of the four tools. Where this page says what a tool does with a construct, it is quoting a row of that matrix, named in code font, such as the row ops_precedence. The matrix is re-run with the command bun run verify matrix. That is a manual command and not part of the automatic build, so the recorded outcomes are those of the last run. Nothing here depends on a vendor tool: Quartus and ModelSim, the tools of the course's labs, are not among the four, and nothing is claimed about how they treat these constructs.

A word about "warning". The book's checks treat a tool warning as a failure unless it is on a short list of reviewed exceptions. When this page says a tool flags a construct, it means an error or a warning of that kind. The outcomes shown from the matrix follow the same rule, so a block headed "error" can quote a message that begins with "Warning".

Almost every module in the book is one of four shapes. The listings below are modules from the chapters.

begin and end. The body of an always block, a branch of an if and an arm of a case each hold exactly one statement. To put two or more statements there, wrap them in begin and end. Leave them out and the second statement does not belong to the branch: under an if it runs on every path, with no complaint from the language, and straight after an always it is not allowed at all, so the file is rejected. A second line added under an if is the classic way to change a design by accident. A listing that has a single statement in a place can leave begin and end out, and the moment that place needs a second statement, they go around all of them.

A module whose outputs depend only on its current inputs. Two forms are used: continuous assign statements, and an always_comb block. In the second form every output gets a value on every path through the block.

gate_ops.sv
// The seven gates of the course as operators, each on its own output.
module gate_ops(input logic a, b,
output logic y_not, y_and, y_or, y_nand, y_nor, y_xor, y_xnor);
assign y_not = ~a;
assign y_and = a & b;
assign y_or = a | b;
assign y_nand = ~(a & b);
assign y_nor = ~(a | b);
assign y_xor = a ^ b;
assign y_xnor = ~(a ^ b);
endmodule
no_latch.sv
// The lamp with a value for every case: the first assignment is the default, and a later one overrides it.
module no_latch(input logic door_open, key_out, output logic lamp);
always_comb begin
lamp = 1'b0;
if (door_open & key_out) lamp = 1'b1;
end
endmodule

In the second module the first assignment gives lamp a value before the if, so no path leaves it unassigned. Section 2.7 explains why that matters, and the section on latches below shows what happens without it.

A module with state. State changes only in an always_ff @(posedge clk) block, which uses the nonblocking assignment <=, with a synchronous active-high reset.

reg4_sync.sv
// A 4-bit register: four flip-flops sharing one clock, with a synchronous reset to 0.
module reg4_sync(input logic clk, reset, input logic [3:0] d, output logic [3:0] q);
always_ff @(posedge clk)
if (reset) q <= 4'd0;
else q <= d;
endmodule

This is a 4-bit register: on each rising edge q takes d, unless reset is 1, in which case it takes a fixed starting value, here 0 (a state machine loads its initial state). The reset is tested inside the always_ff, and that is the form to copy. Section 4.5 builds a timer in which the reset is a multiplexer in front of the adder instead; then the register loads whatever that logic computes, and it need not be 0. Section 4.7 introduces this register with its drawing, and Section 4.8 shows this listing and explains always_ff. Section 5.2 uses the same shape as the last block of a state machine.

A machine has four parts, in this order: the state encoding as an enumeration with a present state ps and a next state ns; the next-state logic as an always_comb block with a case (ps) and an if/else inside the arms that need one; the output logic as an assign or an always_comb; and the state update as an always_ff with the reset.

long_press.sv
// A long-press detector: Out is 1 in the cycle that shows the third 1 in a row.
module long_press(input logic clk, reset, In, output logic Out);
enum logic [1:0] {S0 = 2'b00, S1 = 2'b01, S2 = 2'b10} ps, ns;
// Next state: one arm per state; an if on In picks between a state's arrows. S2 needs none: both its arrows go to S0.
always_comb
case (ps)
S0: if (In) ns = S1;
else ns = S0;
S1: if (In) ns = S2;
else ns = S0;
S2: ns = S0;
default: ns = S0;
endcase
// Output: a Mealy output, so it looks at the input as well as the state.
assign Out = (ps == S2) & In;
// State update: the only place the machine remembers anything.
always_ff @(posedge clk)
if (reset) ps <= S0;
else ps <= ns;
endmodule

Section 5.2 walks through this machine and its diagram. The next-state block assigns ns in every arm and has a default, so no latch appears. The book writes next-state logic as case with if/else, and not as ns = cond ? S1 : S0, because of the tool difference listed in the table of differences below.

A testbench is a module with no ports that instantiates the module under test as dut, drives its inputs, and ends with $stop. Its name is the module's name followed by _tb. For a combinational module the stimulus is a list of input changes separated by delays.

mux2_tb.sv
// Testbench for mux2: four combinations of the inputs, ten time units apart.
module mux2_tb();
logic s, d0, d1, y;
mux2 dut(.*);
initial begin
s = 1; d1 = 0; d0 = 1; #10; // expect: y=0
d1 = 1; #10; // expect: y=1
s = 0; #10; // expect: y=1
d0 = 0; #10; // expect: y=0
$stop;
end
endmodule

A sequential module needs a clock. The clock is generated in a forever loop inside an initial block, with the period in a parameter, and the stimulus waits for each rising edge. Inputs that change on a clock edge are assigned with <=.

long_press_tb.sv
// Testbench for long_press: reset, then fourteen cycles of input, then a reset in the middle of a run.
module long_press_tb();
logic clk, reset, In, Out;
long_press dut(.*);
parameter CLOCK_PERIOD = 100;
initial begin
clk <= 0;
forever #(CLOCK_PERIOD / 2) clk <= ~clk;
end
initial begin
reset <= 1; In <= 0; @(posedge clk);
reset <= 0; In <= 1; @(posedge clk); // expect: ps=S0, Out=0
In <= 1; @(posedge clk); // expect: ps=S1, Out=0
In <= 1; @(posedge clk); // expect: ps=S2, Out=1
In <= 1; @(posedge clk); // expect: ps=S0, Out=0
In <= 1; @(posedge clk); // expect: ps=S1, Out=0
In <= 1; @(posedge clk); // expect: ps=S2, Out=1
In <= 0; @(posedge clk); // expect: ps=S0, Out=0
In <= 1; @(posedge clk); // expect: ps=S0, Out=0
In <= 1; @(posedge clk); // expect: ps=S1, Out=0
In <= 0; @(posedge clk); // expect: ps=S2, Out=0
In <= 1; @(posedge clk); // expect: ps=S0, Out=0
In <= 1; @(posedge clk); // expect: ps=S1, Out=0
In <= 1; @(posedge clk); // expect: ps=S2, Out=1
In <= 0; @(posedge clk); // expect: ps=S0, Out=0
// A second run, interrupted by reset while the machine is in S1.
In <= 1; @(posedge clk); // expect: ps=S0, Out=0
reset <= 1; @(posedge clk); // expect: ps=S1, Out=0
reset <= 0; In <= 1; @(posedge clk); // expect: ps=S0, Out=0
In <= 1; @(posedge clk); // expect: ps=S1, Out=0
In <= 1; @(posedge clk); // expect: ps=S2, Out=1
In <= 0; @(posedge clk); // expect: ps=S0, Out=0
@(posedge clk);
$stop;
end
endmodule

The // expect: comments are checked by the book's own tools on every run, but that is a feature of the book and not of the language: in a testbench of your own they are notes to yourself, and nothing checks them. To make the simulator check a result, compare it in the testbench and print a message, as the self-checking testbench of Appendix A does. Section 3.1 and Section 5.2 explain how to read them and how the stimulus lines are timed. Section 4.8 shows three ways to write the clock, and Section 5.1 covers the timing controls.

Each entry has a one-line meaning, then an example. A construct that no chapter module shows is given as the file of its acceptance-matrix row, with the matrix's banner under the listing, and the outcome the matrix recorded for each tool is shown where it matters.

logic declares a signal, and it is the only signal type the book uses. A module header lists its ports, each with a direction, input or output, and a type. The book writes the ports in the header with their types (the "ANSI" style, as in every listing above). The older style, which lists names in the header and declares directions in the body, is accepted by all four tools (row ports_nonansi). No chapter module is written that way, so this is the matrix file:

ports_nonansi.sv
module ports_nonansi(a, b, y);
input logic a, b;
output logic y;
assign y = a ^ b;
endmodule
A construct the book may use. Non-ANSI port list with separate direction declarations. In the acceptance matrix this file is accepted by every tool that reads it.

wire and reg are legal and every tool accepts them (row bad_wire_reg), but book code never uses them. The book's own rules reject them, with a message like this:

uses_wire.sv
// Declares a net with wire and a variable with reg. Compilers accept both; the book's rule is logic for every signal.
module uses_wire(input logic a, b, output logic y);
wire both;
reg inverted;
assign both = a & b;
always_comb inverted = ~both;
assign y = inverted;
endmodule
Compiles, but breaks a house rule. The file compiles, and the house rules of this book reject it; the message is shown below the listing.
hdl/ch02/s05/uses_wire.sv:3:3: error: [no-wire] use logic, never wire or other net declarations
hdl/ch02/s05/uses_wire.sv:4:3: error: [no-reg] use logic, never reg
Output of the house rules for uses_wire

A signal holds one of four values: 0, 1, X (unknown) and Z (not driven). A sized literal gives a width, a base and digits: 4'd12 is decimal, 4'b1100 binary and 4'hC hex. All four tools accept every form below, and Yosys warns about the Z (row literals_sized), which is why a module that drives Z is waived there.

literal_demo.sv
// Sized literals in three bases, and the two values a bit can have besides 0 and 1.
module literal_demo(output logic [3:0] dec_lit, bin_lit, hex_lit, mixed, unknown, floating);
assign dec_lit = 4'd12;
assign bin_lit = 4'b1100;
assign hex_lit = 4'hC;
assign mixed = 4'b1x0z;
assign unknown = 4'bx;
assign floating = 4'bz;
endmodule

The three spellings of twelve give 4'b1100, 4'b1100 and 4'b1100 in simulation, and 4'b1x0z holds 4'b1x0z. The unknown value is written with a base: 8'bX. The form 8'X, with a size and no base, is rejected by all four tools (row bad_x_literal).

A literal with fewer digits than its size is padded on the left. The padding is 0, except that a literal whose leftmost digit is X or Z is padded with that digit: in simulation 8'bz1 is zzzzzzz1 and 8'bX is eight Xs, which is why 8'bX works as a don't care. A sized literal assigned to a wider signal is padded with zeros, so 4'b1x0z in an 8-bit signal reads 00001x0z. A number with no size, such as 12, has 32 bits.

The bitwise operators work on every bit of their operands. Section 1.2 shows the seven gates as operators, and Section 2.5 writes them as the module shown under the four patterns above.

XNOR is written ~(a ^ b) in book code. The operators ~^ and ^~ also exist, and each is a single XNOR operator at the level of ^. For two operands that makes no difference, since a ^ (~b) is also an XNOR, but next to another operator it does: a ^~ b & c means a ^~ (b & c), not a ^ (~b & c). With a=0, b=1 and c=0 a simulation gave 1 for the first and 0 for the second. The expression toolkit that draws the book's figures follows that rule, and its test checks it against Icarus.

Reduction operators put an operator in front of a single bus and combine its bits into one: &bus is 1 only when every bit is 1, |bus when any bit is 1, ^bus when an odd number of bits are 1, and ~&, ~| and ~^ are the same with the result inverted. All four tools accept them (row ops_reduction).

Arithmetic and comparison: + and - on buses, == for equality and < for less than (row ops_arith_compare). On plain logic vectors, which are unsigned, < compares unsigned numbers: in simulation 4'b1111 < 4'b0001 is 0. Section 4.4 shows how a comparison of signed numbers is built. When the target is no wider than the operands the carry is lost: s = a + b with all three one bit wide silently drops it, and no tool warns about it (row add_carry_dropped); keeping the carry takes a concatenation on the left, {c, s} = a + b (row add_carry_kept).

Conditional: sel ? x : y picks x when sel is 1 and y otherwise. On ordinary values all four tools accept it (row ops_ternary_value). On enumeration values Icarus needs a cast; see the table of differences.

Row Operators What the order rests on
1 (tightest) unary ~ and !, and the reductions & ~& | ~| ^ ~^ ~ above &: the expression test. A reduction above binary + and above |, and ! above == and &&: a simulator run
2 * / % above binary + and -: a simulator run
3 binary + - above << >>, above < and above ==: a simulator run
4 << >> above <: a simulator run
5 < <= > >= above ==: a simulator run
6 == != === !== above &: a simulator run
7 & above ^: the expression test
8 ^ and ~^ (also written ^~) above |: the expression test
9 | above &&: a simulator run
10 && above ||: a simulator run
11 || above ?:: a simulator run
12 (loosest) ? : a simulator run

Description: A table with one row for each group of operators, ordered from unary operators at the top through multiplication, addition, shifts, comparisons, equality, AND, XOR, OR, logical AND, logical OR and the conditional operator at the bottom, with a column saying what the entry rests on.

Figure C.1. Operator precedence for every operator that appears in the book's code, from the tightest-binding row at the top to the loosest at the bottom. Operators on one row bind equally.

Two kinds of evidence stand behind Figure C.1, and they are not equally strong. The relative order of ~, &, ^ (with ~^) and | is checked by the book's own test suite: a test compares the book's expression parser, which gives the figures their bars and their truth tables, with Icarus on 1,000 random expressions and a list of fixed precedence cases, and requires every truth vector to agree. The other adjacent pairs in the table were each checked with an Icarus run while this page was written and again when it was revised, using values chosen so that the two possible groupings give different answers. Those runs are not part of the book's test suite and the page does not reproduce them. Every binary operator in the table groups from the left, so a - b - c is (a - b) - c; only ? : groups from the right. For example, with a=1, b=0 and c=1, 1'b1 ? a : 1'b0 ? b : c gave 1 in a simulator run, which is the value of a and so shows the grouping 1'b1 ? a : (1'b0 ? b : c). The left grouping, (1'b1 ? a : 1'b0) ? b : c, would give the value of b, which is 0.

The table holds every operator that appears in the book's code, and the code uses them unevenly. The designs the chapters show use ~, &, |, ^, the reductions, +, -, == and ? :, and two exercise modules of Chapter 4 also use the right shift >>. The other operators appear only in testbenches and check benches: !==, ===, !=, <, >, >=, &&, ||, !, *, / and %. The loop counter ++ in those testbenches is a statement and is not ranked. A size cast such as 8'(x) gives the value of x the width 8: a wider value loses its high bits, and a narrower unsigned value is padded with zeros on the left. Three Chapter 4 designs use a size cast to make a widening or a narrowing explicit. Each can be written without one, with a concatenation such as {1'b0, a} or a slice of a wider signal, and a search of the text of the course's lectures, section code and lab handouts found no size cast (fact apC/no-size-cast), so do not expect to see one in the course's code. System functions such as $signed, $bits and $time are not operators and have no rank.

Two consequences follow. First, a | b & c means a | (b & c) and a ^ b & c means a ^ (b & c). Section 2.5 shows both in simulation. Second, a comparison binds tighter than &, so a[0] == b[0] & c[0] means (a[0] == b[0]) & c[0]. To mask first and then compare, write a[0] == (b[0] & c[0]).

The book always writes parentheses when it mixes different bitwise operators. slang warns about the bare form, the other three tools accept it (row ops_precedence), and the one page that shows the bare form carries a waiver for that. Section 2.5 shows the warning.

A gate primitive is a gate written as a statement: the gate name, an optional instance name, then the output first and the inputs after it. The names are lowercase: and, or, not, nand, nor, xor, xnor. All four tools accept all seven (row prim_all).

aoi_gates.sv
// The same circuit again, one gate primitive per gate: the output is the first port.
module aoi_gates(input logic A, B, C, D, output logic F);
logic ab, cd, either;
and g1(ab, A, B);
and g2(cd, C, D);
or g3(either, ab, cd);
not g4(F, either);
endmodule

An uppercase AND is not a primitive but an unknown module, and all four tools reject it (row bad_uppercase_primitive).

A bus is declared with a range, logic [7:0] x, and bit 0 is on the right. x[3] is one bit, x[7:4] is a slice, {a, b} joins signals with the leftmost first, and {4{x}} repeats x four times. All four tools accept these (row bus_select_concat).

bus_parts.sv
// One 8-bit bus taken apart and put back together in several ways.
module bus_parts(input logic [7:0] byte_in,
output logic [3:0] high_nibble, low_nibble,
output logic bit_three,
output logic [7:0] swapped, reversed, repeated);
assign high_nibble = byte_in[7:4];
assign low_nibble = byte_in[3:0];
assign bit_three = byte_in[3];
assign swapped = {byte_in[3:0], byte_in[7:4]};
assign reversed = {byte_in[0], byte_in[1], byte_in[2], byte_in[3],
byte_in[4], byte_in[5], byte_in[6], byte_in[7]};
assign repeated = {4{byte_in[1:0]}};
endmodule

Sign extension repeats the top bit, and zero extension prepends zeros.

extend3.sv
// A 3-bit value widened to 6 bits two ways.
module extend3(input logic [2:0] x, output logic [5:0] signext, zeroext);
assign signext = {{3{x[2]}}, x};
assign zeroext = {3'b000, x};
endmodule

A repetition needs a sized operand: a bare number inside {n{...}} has no width, and every tool but Yosys flags it (row bad_unsized_concat).

always_comb describes logic whose outputs follow its inputs, like an assign, and it uses blocking assignments =. The word "runs" belongs to the simulator, which has to model that: it executes the block once at the start of the simulation and again whenever any signal the block reads changes. A multiplexer written three ways shows the forms: if/else, case with default, and the conditional operator.

mux2_if.sv
// The same multiplexer written with if and else.
module mux2_if(input logic s, d0, d1, output logic y);
always_comb begin
if (s) y = d1;
else y = d0;
end
endmodule
mux2_case.sv
// The same multiplexer written with a case statement and a default arm.
module mux2_case(input logic s, d0, d1, output logic y);
always_comb begin
case (s)
1'b0: y = d0;
default: y = d1;
endcase
end
endmodule
mux2_cond.sv
// The same multiplexer as a conditional expression.
module mux2_cond(input logic s, d0, d1, output logic y);
assign y = s ? d1 : d0;
endmodule

A case in book code always ends with a default arm. The design reason is the one in the next section: a value of the selector that no arm handles leaves the output unassigned on that path, so the circuit would have to keep its old value, and that is a latch. The default gives every remaining value an assignment. When the arms already list every value of the selector, the default can never be reached, and it stays so that a later edit that removes an arm cannot open a path with no assignment. A second reason is that slang warns about any case without a default (row bad_case_no_default), and the book treats a warning as a failure.

What the default assigns depends on what the design must promise. When the unlisted inputs can never occur and any output is acceptable, as for the codes a decoder is never given, it can assign an unknown, 8'bX, which says in the code that nothing is promised (row comb_case_x_default, and the seven-segment decoder of Section 3.1). When the design must say what happens, as for the unused codes of a state machine, it assigns a named safe value, as long_press does with ns = S0; Section 5.3 weighs the two. That promise is about the code as written and about the book's simulation, where ns is S0 for any value of ps that no arm names. A synthesis tool may choose a different encoding for the states of a state machine and may remove the logic for codes the machine cannot reach, unless it is told to keep it. (not machine-checked: A property of synthesis tools that nobody here has run or confirmed, including the course's tool; the book's checks do not cover it.) The listing does not show what a synthesized circuit does in an unused code.

An always_comb block that does not assign an output on every path keeps the old value on the paths it skips, and that is a latch. The book never uses a latch on purpose, and its tools reject one.

latch_demo.sv
// A lamp that should be on when the door is open and the key is out. The else case is missing.
module latch_demo(input logic door_open, key_out, output logic lamp);
always_comb begin
if (door_open & key_out) lamp = 1'b1;
end
endmodule
Compiles, but is rejected. This file compiles and slang only prints a warning (inferred-latch). The book treats a warning as a failure, so the checks reject it; the warning is shown below the listing.
hdl/ch02/s07/latch_demo.sv:4:30: warning: latch inferred for 'lamp' because it is not assigned on all control paths [-Winferred-latch]
    if (door_open & key_out) lamp = 1'b1;
                             ^~~~
Output of slang for latch_demo

The same holds for an if with no else (row bad_if_no_else) and for a case with no default that misses some values (row bad_case_no_default). slang, Verilator and Yosys flag both; Icarus accepts both.

A related mistake is reading a variable in an always_comb before the block assigns it (row bad_comb_order): only Verilator flags it, and the book's code computes each value before it is used.

bad_comb_order.sv
module bad_comb_order(input logic a, b, output logic y);
logic t;
always_comb begin
y = t | a;
t = a & b;
end
endmodule
A construct the book avoids. Always_comb reads a variable assigned later in the same block. In the acceptance matrix this file is rejected by at least one tool.
verilator: warning (not waived)
verilator prints a warning: %Warning-ALWCOMBORDER: always_comb reads 't' before assigning it later in the same block; behavior m. No allowlist entry of this book waives it, so the checks reject the file.
Recorded outcome of verilator for the construct "always_comb reads a variable assigned later in the same block" (matrix row bad_comb_order)

always_ff @(posedge clk) describes flip-flops that take new values at each rising edge of clk, and it uses <=. As with always_comb, "runs" is the simulator's word: it executes the block at each rising edge. The reset in book code is synchronous: it is tested inside the block, as in the register above (row ff_sync_reset). An asynchronous reset, always_ff @(posedge clk or posedge reset), is accepted by all four tools (row ff_async_reset) and the book does not use it in its designs.

enum logic [1:0] {S0 = 2'b00, S1 = 2'b01, S2 = 2'b10} ps, ns; declares named constants and two variables that hold them, as in the state machine above. With no explicit values the constants count up from 0 (row enum_default_codes). Assigning a plain number to an enumeration variable is rejected by slang, Verilator and Icarus and accepted by Yosys (row bad_enum_int_assign).

An instance puts one module inside another. The three ways to connect the ports are by name, .A(A); by implicit name, .A, when the signal has the same name; and .* for every port whose name matches a signal.

connect_forms.sv
// Three ways to connect the same instance: by name, by implicit name, and with .* for every port that matches.
module connect_forms(input logic A, B, C, output logic m_named, m_implicit, m_star);
majority_three u_named(.A(A), .B(B), .C(C), .M(m_named));
majority_three u_implicit(.A, .B, .C, .M(m_implicit));
majority_three u_star(.*, .M(m_star));
endmodule

All three forms are accepted by all four tools (row inst_forms). An instance sits in the module body and never inside an always block (row bad_module_in_always: all four tools reject it).

A parameter makes a width or a count a property of the module: module m #(parameter N = 4) (...), instantiated with m #(.N(8)) u(...). Section 4.2 introduces parameters with this adder:

add_n.sv
// An adder whose width is a parameter. The sum is N bits and the carry out of the top bit is a separate output.
module add_n #(parameter N = 4) (
input logic [N-1:0] a,
input logic [N-1:0] b,
output logic [N-1:0] sum,
output logic carry
);
assign {carry, sum} = a + b;
endmodule

No chapter module uses generate or arrays, so the listings below are the files of the acceptance matrix. The first two are a module with a default value and an instance that overrides it (row param_override):

param_module.sv
module param_module #(parameter N = 4) (input logic [N-1:0] a, output logic [N-1:0] y);
assign y = ~a;
endmodule
A construct the book may use. Parameterized module with a default value, overridden on instantiation. In the acceptance matrix this file is accepted by every tool that reads it.
param_user.sv
module param_user(input logic [7:0] a, output logic [7:0] y_wide, output logic [3:0] y_narrow);
param_module #(.N(8)) wide(.a(a), .y(y_wide));
param_module narrow(.a(a[3:0]), .y(y_narrow));
endmodule
A construct the book may use. Parameterized module with a default value, overridden on instantiation. In the acceptance matrix this file is accepted by every tool that reads it.

A generate block with a genvar repeats hardware (row gen_for):

gen_for.sv
module gen_for(input logic [3:0] a, b, output logic [3:0] y);
genvar i;
generate
for (i = 0; i < 4; i++) begin : bit_slice
xor g(y[i], a[i], b[i]);
end
endgenerate
endmodule
A construct the book may use. Generate with genvar and a labelled block. In the acceptance matrix this file is accepted by every tool that reads it.

Arrays hold several values of one type. This file has a two-dimensional packed array and an unpacked array of words (row arrays_packed_unpacked):

arrays_packed_unpacked.sv
module arrays_packed_unpacked(input logic [1:0] sel, input logic [3:0][1:0] packed_in, output logic [1:0] pick, output logic [7:0] flat);
logic [1:0] words [0:3];
always_comb begin
words[0] = packed_in[0];
words[1] = packed_in[1];
words[2] = packed_in[2];
words[3] = packed_in[3];
pick = words[sel];
end
assign flat = {packed_in[3], packed_in[2], packed_in[1], packed_in[0]};
endmodule
A construct the book limits. Packed 2-D and unpacked arrays. In the acceptance matrix this file is accepted with a waived warning.

Parameters and generate are accepted by all four tools without a warning (rows param_override, param_local and gen_for). For arrays, Yosys and Icarus each warn about one construct, and slang and Verilator accept the file. These are the recorded outcomes of the two warnings:

yosys: warning
yosys accepts it with a warning, which the allowlist of this book waives: An unpacked array of small words (Chapter 9) that Yosys turns into registers; fine for a lint pass.. Without that waiver the checks of this book would reject the file.
Recorded outcome of yosys for the construct "packed 2-D and unpacked arrays" (matrix row arrays_packed_unpacked)
icarus: warning
icarus accepts it with a warning, which the allowlist of this book waives: Icarus makes the process sensitive to the whole vector (conservative); behavior is correct, only the sensitivity list is wider.. Without that waiver the checks of this book would reject the file.
Recorded outcome of icarus for the construct "packed 2-D and unpacked arrays" (matrix row arrays_packed_unpacked)

These appear in testbenches and not in designs: initial blocks, #10 delays, forever, wait, for loops with an int variable, $display, $stop and $finish. A plain always for the clock is accepted (row tb_clock_always). A testbench whose clock runs in a forever loop never ends unless it calls $stop or $finish; the book's runner kills such a run after five seconds (row bad_tb_no_stop), and the testbench rules reject a testbench whose stimulus does not end with $stop or $finish.

The four tools do not accept exactly the same language, and a design that passes one may fail another. The book's code is written so that it passes all four. The table lists every row of the matrix in which the tools do not all give the same kind of outcome (accepted, accepted with a waived warning, or flagged), and two more rows in which all four flag a construct but one only with a warning, and one row in which all four accept a real mistake (add_carry_dropped), with what the book does about each. Rows in which every tool that runs the file gives the same outcome are not here, and the rows in which only slang's waived unused-output warning on the testbench differs (fsm_if_else, tb_delay_loop, tb_clock_always, tb_clock_blocking, tb_enum_name) are left out because they say nothing about the language. "Flag" means an error or a warning of the kind the book's checks treat as a failure.

Construct What differs What the book does
ns = cond ? S1 : S0 where ns is an enumeration (row enum_ternary) slang, Verilator and Yosys accept it; Icarus rejects it with "This assignment requires an explicit cast." Next-state logic is case with if/else in each arm. The conditional form appears once, in an aside that is proved equivalent by Yosys and never simulated
Mixed bitwise operators with no parentheses (row ops_precedence) only slang warns Parentheses wherever different operators mix
A literal with a Z digit (row literals_sized) only Yosys warns, about limited support for tri-state logic A module that drives Z carries a waiver for the warning
A blocking = in always_ff, or a <= in always_comb (rows bad_blocking_in_ff, bad_mixed_assign_ff, bad_nonblocking_in_comb) only Verilator flags the first two; Verilator and Icarus flag the third; slang and Yosys flag none of them = in always_comb and <= in always_ff, never mixed in one block
A variable read in an always_comb before the block assigns it (row bad_comb_order) only Verilator flags it Each value is computed before it is used
A 2-bit signal compared with a 3-bit constant (row bad_width_expand) only Verilator flags it Widths are written to match
A wide value assigned to a narrower signal (row bad_width_trunc) slang and Verilator flag it; Yosys and Icarus accept it the same
A declaration with an initial value (row bad_initializer) only Verilator flags it No initializers in design files; the house rules reject them
A second @(posedge clk) inside an always_ff (row bad_event_in_ff) slang, Yosys and Icarus reject it; Verilator accepts it One event control, at the top
A port named in the body and missing from the header (row bad_port_not_in_list) Icarus accepts it; the other three reject it Every port is in the header
Assigning to an input (row bad_assign_to_input) Icarus accepts it; the other three flag it Never done
A trailing comma before the closing parenthesis of a port list (row bad_trailing_comma) Yosys accepts it; slang and Icarus reject it, and Verilator flags it with a warning (NULLPORT) No trailing comma
An unused signal or an undriven output (row bad_unused_undriven) Icarus accepts it; the other three flag it Every output is driven
Two always_comb blocks driving one variable (row bad_two_always) Icarus accepts it; the other three flag it One driver for every signal
An if with no else, or a case with no default that misses values (rows bad_if_no_else, bad_case_no_default) slang, Verilator and Yosys flag both; Icarus accepts both Every path assigns, and every case has a default
A plain number assigned to an enumeration variable (row bad_enum_int_assign) slang, Verilator and Icarus flag it; Yosys accepts it Enumeration constants only
An unsized constant in a replication (row bad_unsized_concat) slang, Verilator and Icarus flag it; Yosys accepts it Every operand of a repetition has a size
An undeclared name (rows bad_undeclared_ident, bad_undeclared_ns) slang, Verilator and Icarus reject it; Yosys only warns and carries on (for the undeclared signal, that it is implicitly declared; for ns, that a wire is assigned in a block) Every signal is declared
Packed and unpacked arrays (row arrays_packed_unpacked) Yosys warns that it replaces a memory with registers, and Icarus warns about a constant select in an always block A module that uses them carries a waiver for the warning
An always_comb that assigns only constants (row comb_constant) only Icarus warns, that the block has no sensitivities The warning is waived everywhere in the book's checks, because the block still drives its value once at time 0
$deposit into an enumeration variable (row tb_deposit_enum) slang rejects it; Icarus runs it Generated check benches only, compiled by Icarus alone
A dropped carry in s = a + b (row add_carry_dropped) no tool flags it A self-checking testbench or an // expect: comment is the only protection
A testbench with a forever clock and no $stop (row bad_tb_no_stop) slang gives only its waived unused-output warning; Icarus runs until the runner gives up after five seconds Every testbench ends with $stop

Three of these rows are shown with their files. The conditional on an enumeration:

enum_ternary.sv
module enum_ternary(input logic clk, reset, in, output logic out);
enum logic [1:0] {S0 = 2'b00, S1 = 2'b01, S2 = 2'b10} ps, ns;
always_comb
case (ps)
S0: ns = in ? S1 : S0;
S1: ns = in ? S2 : S0;
S2: ns = in ? S0 : S0;
default: ns = S0;
endcase
assign out = (ps == S2) & in;
always_ff @(posedge clk)
if (reset) ps <= S0;
else ps <= ns;
endmodule
A construct the book limits. Enum-valued ?: assigned to an enum: Icarus needs an explicit cast, the others accept (the nosim aside). In the acceptance matrix this file is rejected by at least one tool.
icarus: error
icarus rejects it with an error: error: This assignment requires an explicit cast.
Recorded outcome of icarus for the construct "enum-valued ?: assigned to an enum: Icarus needs an explicit cast, the others accept (the nosim aside)" (matrix row enum_ternary)

An undeclared ns, which Yosys reports only as a warning:

bad_undeclared_ns.sv
module bad_undeclared_ns(input logic clk, reset, go, output logic busy);
enum logic [1:0] {IDLE, RUN, DONE} ps;
always_comb
case (ps)
IDLE: if (go) ns = RUN;
else ns = IDLE;
default: ns = IDLE;
endcase
assign busy = (ps == RUN);
always_ff @(posedge clk)
if (reset) ps <= IDLE;
else ps <= ns;
endmodule
A construct the book avoids. A state machine that never declares ns. In the acceptance matrix this file is rejected by at least one tool.
yosys: warning (not waived)
yosys prints a warning: Warning: wire '\ns' is assigned in a block at hdl/_infra/matrix/bad_undeclared_ns.sv:5.21-5.29.. No allowlist entry of this book waives it, so the checks reject the file.
Recorded outcome of yosys for the construct "a state machine that never declares ns" (matrix row bad_undeclared_ns)

An initializer, which only Verilator flags:

bad_initializer.sv
module bad_initializer(input logic clk, d, output logic q);
logic r = 1'b0;
always_ff @(posedge clk) r <= d;
assign q = r;
endmodule
A construct the book avoids. Declaration initializer in a design file (accepted by the tools; the house rule rejects it). In the acceptance matrix this file is rejected by at least one tool.
verilator: warning (not waived)
verilator prints a warning: %Warning-PROCASSINIT: Procedural assignment to declaration with initial value: 'r'. No allowlist entry of this book waives it, so the checks reject the file.
Recorded outcome of verilator for the construct "declaration initializer in a design file (accepted by the tools; the house rule rejects it)" (matrix row bad_initializer)

Here is the width case as Verilator reports it. The module compares a 2-bit signal with 3-bit constants. slang, Yosys and Icarus accept it.

count_wide_bad.sv
// A 2-bit count compared with 3-bit constants.
module count_wide_bad(input logic [1:0] count, input logic A, B, output logic M);
always_comb begin
if (count == 3'd1) M = A;
else if (count == 3'd2) M = B;
else M = 1'b0;
end
endmodule
Compiles, but is rejected. This file compiles and Verilator only prints a warning (WIDTHEXPAND). The book treats a warning as a failure, so the checks reject it; the warning is shown below the listing.
%Warning-WIDTHEXPAND: hdl/ch02/s07/count_wide_bad.sv:5:20: Operator EQ expects 3 bits on the LHS, but LHS's VARREF 'count' generates 2 bits.
                                                         : ... note: In instance 'count_wide_bad'
    5 |     else if (count == 3'd2) M = B;
      |                    ^~
                      ... For warning description see https://verilator.org/warn/WIDTHEXPAND?v=5.055
                      ... Use "/* verilator lint_off WIDTHEXPAND */" and lint_on around source to disable this message.
%Warning-WIDTHEXPAND: hdl/ch02/s07/count_wide_bad.sv:4:15: Operator EQ expects 3 bits on the LHS, but LHS's VARREF 'count' generates 2 bits.
                                                         : ... note: In instance 'count_wide_bad'
    4 |     if (count == 3'd1) M = A;
      |               ^~
Output of Verilator for count_wide_bad

Each rule below is one the book's code never breaks. For each, the table says what the tools do with a violation, as recorded in the matrix. Where every tool accepts the code, the rule is enforced by the book's own checks instead, which are called the house rules.

Do not Why What the tools do
Use plain always in a design always_comb and always_ff say what the block is, and the tools check that it behaves that way all four accept it (row bad_plain_always); the house rules reject it. Testbenches may use always for a clock
Use wire or reg logic does both jobs all four accept them (row bad_wire_reg); the house rules reject them
Mix = and <= in one block, or use = for state blocking assignments in a clocked block can make a value change before every flip-flop has read the old one Verilator flags a blocking assignment in always_ff (rows bad_blocking_in_ff, bad_mixed_assign_ff); the other three accept it
Assign one variable from more than one always_comb, assign or module output two drivers on one signal slang, Verilator and Yosys flag two always_comb blocks, and all four flag two assign statements and an assign plus a module output (rows bad_two_always, bad_two_assigns, bad_module_and_assign); Icarus accepts the two-always_comb case
Instantiate a module inside an always block an instance is hardware, not a statement all four reject it (row bad_module_in_always)
Assign to a module input the value comes from outside Verilator rejects it; slang and Yosys flag it; Icarus accepts it (row bad_assign_to_input)
Put @(posedge clk) anywhere but the top of an always_ff a second event control is not hardware row bad_event_in_ff, in the table of differences above
Gate a clock, or use a signal that is not the clock as one a clock made by logic can glitch and the design stops being one design all four accept it (row bad_clock_gated); the house rule no-clock-gating rejects it
Clock different blocks from different clocks the whole design runs on one clock all four accept it (row bad_two_clocks); the house rule one-clock rejects it
Leave a path without an assignment in always_comb a latch slang, Verilator and Yosys flag it (rows bad_if_no_else, bad_case_no_default)
Forget to declare ns an undeclared name slang, Verilator and Icarus reject it; Yosys only warns, that a wire ns is assigned in a block, and carries on (row bad_undeclared_ns)
Use #delay in a design file a delay has no meaning in hardware all four accept it (row bad_delay_in_design); the house rules reject it outside the simulation-only delay models
Put an initial value on a declaration in a design file state gets its starting value from reset, so the design does not depend on what the chip holds at power-up only Verilator flags it (row bad_initializer); the house rules reject it
Skip the testbench, or end it without $stop an unsimulated module is an untested one, and a testbench that never stops never finishes row bad_tb_no_stop: the run is killed after five seconds
Leave a testbench input undriven at time 0 the module sees X the testbench rules reject it; section 3.2 shows the result

The reasons in the "Why" column are explained at greater length in the sections that introduce each topic.