RTL design fundamentals

Synthesizable Verilog: What Synthesizes and What Doesn't

Verilog is two languages wearing one syntax. One half describes hardware that a synthesis tool can turn into gates and flip-flops; the other half describes simulation behaviour — delays, file I/O, messages, dynamic data structures — that has no silicon equivalent. Mixing them is the single most common reason RTL "works in simulation" and then fails, behaves differently, or is rejected outright at synthesis. This page walks the synthesizable subset, the constructs that never synthesize, the gotchas that synthesize into the wrong hardware, and how lint and synthesis catch all three early.

The synthesizable subset: what maps to hardware

Synthesis is structural. Every line of synthesizable RTL has to reduce to one of a handful of primitives: a combinational gate, a register (edge-triggered flip-flop), a latch (level-sensitive), a multiplexer, or a memory. The synthesizable subset is the set of constructs that can be lowered to those primitives unambiguously:

  • Continuous assignments (assign) and combinational always @* / always_comb blocks — map to gates and muxes.
  • Edge-triggered always @(posedge clk) / always_ff blocks with non-blocking assignments — map to flip-flops.
  • Bitwise, arithmetic, relational, shift, reduction, and ternary operators — map to the corresponding gate logic and datapath cells.
  • if / case (including unique /priority) — map to mux trees.
  • Constant-bound loops and generate — unrolled at elaboration into repeated structure.
  • Parameters, localparams, packed arrays, and memory arrays — map to widths, wiring, and RAM/ROM inference.

Almost everything else is either non-synthesizable or a gotcha that synthesizes into hardware you did not intend. For the assignment-style rule that decides whether a block becomes flops or gates, see blocking vs non-blocking assignment.

What does not synthesize

These constructs are legal Verilog and useful in testbenches, but a synthesis tool either ignores them, errors on them, or — worst — quietly drops them, leaving you with silicon that does not match the simulation you signed off on:

  • Delays: #5, #(period), and timing controls. Silicon timing comes from the cell library and the clock tree, not from # delays. Synthesis strips them.
  • Initial blocks for logic: initial describes a one-shot at t=0. ASIC synthesis ignores it; FPGA flows honour it only for RAM/register init via the bitstream. Use a reset, not an initial, for power-up state.
  • System tasks: $display, $finish, $fopen, $random, $readmemh (outside memory init). Simulation-only.
  • Floating-point: the real and realtime types. There is no floating-point in plain RTL — use fixed-point integers.
  • Fine-grained parallelism: fork / join, wait, event-controlled procedural blocks beyond a single clock edge.
  • Dynamic constructs: dynamic arrays, queues, associative arrays, classes, mailbox, new, recursion, and most of the SystemVerilog verification layer (constraints, covergroups).
  • Non-constant loop bounds: a loop whose iteration count depends on a run-time signal cannot be unrolled and is not synthesizable.

Synthesizable vs not: side by side

The two modules below have the same intent — an 8-bit free-running counter — but only one of them is hardware. The first reads fine in a simulator and is a legitimate testbench style; it is not RTL.

// NON-SYNTHESIZABLE: this reads cleanly in a simulator and is a
// perfectly good *testbench*, but none of it maps to gates.
module pulse_gen_bad (output reg clk, output reg [7:0] count);
    initial clk = 1'b0;            // initial: no t=0 in silicon
    always #5 clk = ~clk;          // #5 delay: not synthesizable
    real period = 10.0;            // 'real': no floating-point hardware
    initial begin
        count = 0;                 // power-up via initial, not reset
        $display("starting");      // $display: simulation-only
        #1000 $finish;             // $finish: simulation control
    end
endmodule

The synthesizable version takes the clock as an input, gives the counter a real reset, and uses non-blocking assignment so the body becomes flip-flops rather than a chain of combinational logic:

// SYNTHESIZABLE: the same intent expressed in the RTL subset.
// A counter built from real flip-flops with a real reset. The clock is
// an input (generated by a PLL/clock tree), not something RTL toggles.
module pulse_gen_good (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [7:0] count
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 8'd0;          // reset value -> real reset network
        else
            count <= count + 8'd1;  // adder + 8 flip-flops
    end
endmodule

The dangerous middle: code that synthesizes into the wrong thing

The non-synthesizable constructs above are the easy case — a tool rejects them and you fix them. The expensive bugs are the constructs that do synthesize, just not into the hardware you pictured. These pass simulation, pass elaboration, and quietly build the wrong gates.

Inferred latches

When a combinational block does not assign a signal on every control path, synthesis must hold the old value — so it builds a level-sensitive latch. Latches are a verification and timing hazard, and an inferred one is almost always a bug:

// GOTCHA: incomplete combinational assignment infers a latch.
// 'y' is not assigned on the missing 2'b11 path, so synthesis builds a
// level-sensitive latch to hold the old value -- almost never intended.
always @* begin
    case (op)
        2'b00: y = a;
        2'b01: y = b;
        2'b10: y = c;
        // no 2'b11, no default -> latch on 'y'
    endcase
end

The fix is a default assignment before the case or a default: arm. We cover the four canonical latch-inferring patterns in inferred latches in Verilog.

Incomplete sensitivity lists

A combinational block written with an explicit sensitivity list (always @(a or b)) that omits a signal it reads produces a simulation/synthesis mismatch: the simulator only re-evaluates on the listed signals, while synthesis builds combinational logic that reacts to all of them. The simulation looks like a latch; the netlist does not. Use always @* or always_comb so the tool computes the sensitivity list for you.

Multiple drivers

Two always blocks (or an assign and an always) writing the same signal is a frequent merge-conflict aftermath. Simulation may pick one; synthesis reports a multi-driver conflict or builds bus-contention logic. A single signal should have exactly one driver.

Non-constant loops and X-assignment

A for loop with a run-time bound cannot be unrolled — it either errors or, with a tool-guessed maximum, builds far more hardware than you meant. Assigning x as a don't-care is sometimes deliberate, but it is a classic simulation/synthesis divergence: the simulator propagates the unknown, synthesis optimises it to a concrete 0 or 1, and the two no longer agree. Both are width- and intent-adjacent to the bugs covered in width mismatches in Verilog.

How lint and synthesis catch this early

You do not have to wait for a commercial synthesis run to find these. Two open-source layers catch the bulk of non-synthesizable and poorly-synthesizing RTL before it leaves your branch:

  • Lint works at the source level, before elaboration. A good lint pass flags initial blocks in synthesizable scope, delays, incomplete case statements, incomplete sensitivity lists, and non-constant loop bounds — fast enough to run on every commit. See the RTL lint checklist for the rules worth enforcing as errors.
  • Synthesis is the ground truth: if a construct does not map to a cell, the tool says so. Yosys elaborates the RTL, runs proc; opt; check, and emits the latch-inferred, multi-driver, and undriven warnings that tell you the design will or will not synthesize cleanly. A check -assert turns those warnings into a non-zero exit — the form CI should run.

Together these are structural evidence that your RTL is in the synthesizable subset and elaborates without inferring surprise hardware. For where this sits in the wider flow alongside simulation and formal, see our guide to pre-signoff RTL verification.

A practical synthesizable-RTL checklist

  • No initial blocks for state — reset everything you rely on.
  • No # delays, no $display/$finish, no real.
  • Combinational blocks use always @*/always_comb with a default on every output.
  • Sequential blocks use non-blocking <= on a single clock edge.
  • Every loop bound is a constant; every signal has exactly one driver.
  • Run lint on every commit and a synthesis elaboration with check -assert in CI.

FAQ

What is synthesizable Verilog?

Synthesizable Verilog is the subset of the language that a logic-synthesis tool can map onto real hardware: combinational gates, flip-flops, latches, multiplexers, and memories. It excludes anything that only has meaning to a simulator — delays, file I/O, unbounded loops, dynamic memory, and most of the verification side of SystemVerilog. The IEEE 1364.1 / 1800 RTL synthesis subset is the formal definition, but in practice the synthesizable subset is whatever your synthesis tool (Yosys, Design Compiler, Genus) accepts and maps to cells without error.

Why won't an initial block synthesize?

An initial block describes a one-time event at simulation time t=0. Real silicon has no t=0 event you can use to load arbitrary state — power-up values come from a reset sequence or, on FPGAs, from the bitstream. Most ASIC synthesis tools ignore initial blocks entirely or error on them. FPGA toolchains (Vivado, Yosys' FPGA flows) honour initial blocks for RAM/ROM initialization and register power-up because the bitstream can carry that state, which is exactly why RTL that 'works in simulation' because of an initial block can behave differently after ASIC synthesis.

Is a for-loop synthesizable?

Yes, when its bounds are constant (elaboration-time). Synthesis unrolls the loop into N copies of the body, so the loop count must be known at compile time — a parameter, a localparam, or a literal. A loop whose bound depends on a run-time signal cannot be unrolled and is not synthesizable. The same applies to generate-for loops, which are unrolled structurally. If you see a loop indexed by a value that changes during operation, that is a non-synthesizable construct, not just bad style.

Does ChipVerify AI guarantee my RTL will synthesize on my foundry's flow?

No. ChipVerify AI runs open-source synthesis (Yosys) and lint over your RTL and reports the structural evidence it finds — inferred latches, multi-driver nets, undriven signals, non-synthesizable constructs the open frontend rejects — with file-and-line findings. That is pre-signoff evidence that the RTL elaborates and maps cleanly on open tools, not a foundry signoff and not a guarantee about a specific commercial synthesis flow or PDK. Run it alongside your vendor synthesis, lint, and simulation, not as a replacement for them.

Are X-assignments synthesizable?

Assigning x (e.g. y = 1'bx) is legal RTL and synthesis treats x as a don't-care, which it is free to optimise to 0 or 1. That can be deliberate — flagging unreachable case items as don't-cares helps the optimiser. The danger is the simulation/synthesis mismatch: the simulator propagates x and may show a known failure, while synthesis picks a concrete value and hides it, or vice versa. Tools like Yosys and X-propagation lint surface intentional vs accidental x so reviewers can decide which don't-cares are safe.

Scan your RTL

Paste a GitHub URL or drop a file at chipverify.ai/tinytapeout and ChipVerify AI runs Yosys elaboration plus lint over the design, reporting inferred latches, multi-driver nets, undriven signals, and constructs the open synthesis frontend rejects — each with a file-and-line reference. No install, no synthesis script to tune.

Check your RTL is in the synthesizable subset

Sign in and point ChipVerify AI at your Verilog or SystemVerilog. It runs Yosys synthesis and lint to surface inferred latches, multi-driver nets, and non-synthesizable constructs — pre-signoff structural evidence with file-and-line findings, not a foundry signoff.