What "Synthesizable" Means

SystemVerilog has a secret it does not print on the box: it is two languages wearing one syntax. One dialect — the synthesizable subset — describes hardware that a synthesis tool can turn into gates. The other dialect exists only in simulation: delays, test scaffolding, printing, spawning parallel processes. Both dialects compile; both simulate; only one becomes silicon. Nothing in the language stops you writing a design that simulates like a dream and synthesizes into garbage — or into nothing at all. Knowing exactly where the border runs is what separates people who write RTL from people who write wishes.

The border, on one table

ConstructSynthesizable?Why
assign, always_combyesgate networks
always_ff @(posedge clk)yesflip-flops
if/case, operators, slicesyesmuxes and logic
module instances, parameter, generateyesstructure, frozen at elaboration
#10 delaysnosilicon has no delay knob — timing comes from physics
initial blocksno"run once at time zero" — that is a testbench's job
$display, $error, $randomnosimulator services; there is no console in a chip
fork…join, wait, file I/Onosimulation processes, not hardware

The sim-only rows are not defects — they are the verification half of the language, and the next module lives there happily. The sin is not using them; the sin is using them in the design and believing the simulation.

The map

The classic traps

Four failure modes account for most synthesizability grief. You have met the first already:

1. Inferred latches. A combinational block that fails to assign an output on some path forces the tool to build memory you never asked for. The cure is mechanical — a default assignment at the top of the block, an else for every trailing if, a default arm in every case — and always_comb warns you when you slip.

2. Simulation/synthesis mismatch. The simulator and the synthesizer can honestly disagree about what your text means. The classic sources: a legacy always @(a) block that reads b too — the simulator only re-evaluates when a changes (stale b!) while the synthesizer builds gates that respond to both; and zero-delay evaluation order between blocks, which the simulator resolves by picking an order while real gates just settle. Sprinkling #10 delays to "fix" a misbehaving design is the anti-pattern of anti-patterns: it changes the simulation and leaves the hardware untouched.

3. X-propagation. Simulation's four-valued logic (0, 1, x, z) is an honest fiction. Real silicon always holds a definite 0 or 1 — x means the simulator does not know which. The fiction errs in both directions: x-optimism (some constructs quietly swallow an x — a case falls to its default, an if (x) takes the else — hiding a real bug the silicon will express), and x-pessimism (an x spreads through logic that would in fact produce a definite value: x & 0 is really 0). Treat any x reaching real logic as radioactive; find the unreset register it leaked from.

4. Multiple drivers. Two always blocks assigning one signal is legal-ish text describing two gate outputs soldered to the same wire — in silicon, a fight between a transistor pulling high and one pulling low: a short circuit. One signal, one driving block, always.

See a mismatch happen

Here is trap 2 in miniature. Two combinational blocks: b = NOT a, and y = a AND b. The hardware answer is eternally y = a \land \lnot a = 0. But a zero-delay simulator that evaluates each block once, in some order after a changes can read a stale b:

// Two "always blocks", as functions writing shared signals. interface Sig { a: number; b: number; y: number } const blockB = (s: Sig) => { s.b = s.a ^ 1; }; // b = ~a const blockY = (s: Sig) => { s.y = s.a & s.b; }; // y = a & b (in hardware: always 0) // A naive simulator: after a changes, run each block ONCE in a chosen order. function naiveSim(order: Array<(s: Sig) => void>): number { const s: Sig = { a: 0, b: 1, y: 0 }; // settled state for a = 0 s.a = 1; // the input changes... for (const blk of order) blk(s); // ...one pass, in this order return s.y; } console.log("naive sim, order B then Y:", naiveSim([blockB, blockY])); // 0 (fresh b) console.log("naive sim, order Y then B:", naiveSim([blockY, blockB])); // 1 (stale b!) // The hardware has no order: it iterates until nothing changes (a fixed point). function settle(): number { const s: Sig = { a: 1, b: 1, y: 0 }; for (let pass = 0; pass < 10; pass++) { blockB(s); blockY(s); } return s.y; } console.log("hardware (settled): ", settle()); // 0, always

Same description, two different simulated answers depending on an ordering the hardware does not have. Real event-driven simulators are far cleverer than this naive one — but the lesson stands: when your design's correctness depends on which block the simulator ran first, you have described a race, not a circuit. (This is precisely why the iron rule from sequential logic<= in always_ff — exists: non-blocking assignment removes the ordering from register updates.)

The lint mindset

You do not police this border alone. A lint tool (open-source Verilator's lint mode, or the commercial linters every chip company runs) reads your RTL before simulation and flags the whole rogues' gallery: inferred latches, multiple drivers, width mismatches, incomplete cases, sim-only constructs in design files. Professional flows make lint-clean a precondition for even running a simulation. Treat the linter as your first code reviewer — the one who never gets tired and is never polite.

Because a real gate's delay is not a number you choose — it is physics you suffer: a function of the transistors' size, the wire lengths after placement, the supply voltage, the chip's temperature, and manufacturing luck, varying part to part and hour to hour. That is why the synthesizable world never says "wait 10 nanoseconds"; it says "wait one clock edge", and then a separate discipline (static timing analysis, later in this course) proves that every combinational path settles inside the clock period across all those conditions. The delays in your simulation are cartoons for your convenience. When a design only works with a #10 in it, the #10 is a confession: somewhere there is an ordering assumption the hardware will not honour, and the fab will collect on it.

The deadliest sentence in chip design. Simulation checks the description as the simulator interprets it — including every sim-only construct, every x-optimism, every lucky evaluation order. Synthesis then builds a different interpretation: the subset, x-free, orderless. The overlap is large, and the industry's flows exist to make it total — lint, synthesizable-subset discipline, and later formal equivalence checks between RTL and gates. But the burden of proof runs one way: a clean simulation of undisciplined RTL proves nothing about the silicon. Chips have shipped with bugs that "could not happen" because they never happened in simulation — an unreset register that woke up differently on the bench than in the fab's silicon, a race between two always blocks that the simulator happened to order kindly. Discipline first; then simulate; then trust — a little.