Verilog · Chapter 7.4 · Compiler Directives
Conditional Compilation in Verilog — `ifdef / `ifndef / `else / `elsif
Conditional compilation is Verilog's preprocessor-layer mechanism for including or excluding source text based on whether a macro is defined. The five directives, ifdef, ifndef, else, elsif, and endif, let a single source file carry multiple build targets: simulation-only debug code that vanishes from synthesis, RTL variants gated by feature flags, target-specific code that depends on the tool or device, debug versus release builds, and per-customer customisation cuts. The mechanism is borrowed from C and works the same way, with the preprocessor deciding what survives and then handing the surviving text to the compiler. This page is the deep drill on the directive family: the syntax, the simulation-versus-synthesis discipline, real-world build-target patterns, the nesting rules, and the pitfalls that compound silently when conditional cuts get sloppy.
Foundation17 min readVerilogifdefConditional CompilationPreprocessorBuild Targets
Chapter 7 · Section 7.4 · Compiler Directives
1. The Engineering Problem
A verification engineer adds a $display statement to dump every memory read in the cache controller. The line catches a regression two hours later — a stale tag was returning a hit. Fix applied, debug line kept "just for now."
Six months later, the RTL goes through synthesis. The synthesis tool warns: $display is a non-synthesisable system task — ignored. The warning is dismissed (it's just a warning). At post-route timing analysis, the design hits a violation. Investigation reveals the synthesiser didn't simply ignore the $display — it deleted the entire block containing it, including a one-cycle delay net that the violation depended on. The "harmless" debug line caused a real synthesis defect.
The right fix isn't to delete the $display. It's to gate it on a build-target macro so it lives in simulation builds and disappears in synthesis builds:
`ifdef SIMULATION
$display("[cache] read tag=%h hit=%b", tag, hit);
`endifIn a simulation build (+define+SIMULATION on the simulator command line), the $display survives the preprocessor and runs. In a synthesis build (no SIMULATION macro defined), the preprocessor strips the entire \ifdef SIMULATION ... `endif` block — the synthesis tool never sees it.
This is the canonical role of conditional compilation: single source file, multiple build targets. RTL variants, debug code, target-platform branches, customer-specific cuts — all gated by macros so the preprocessor decides what survives. This page is the deep drill on the directive family — \ifdef, `ifndef, `else, `elsif, `endif` — and the discipline that keeps the cuts maintainable as the codebase grows.
2. Anatomy of Conditional Compilation
The directive family has five members.
2.1 `ifdef — include if defined
`ifdef MACRO_NAME
// This text is included if MACRO_NAME has been `defined.
`endifIf MACRO_NAME is defined (via `define MACRO_NAME, command-line +define+MACRO_NAME, or another \ifdefcascade), the text between`ifdefand`endif` is included. Otherwise it is stripped before compilation.
2.2 `ifndef — include if NOT defined
`ifndef MACRO_NAME
// This text is included if MACRO_NAME has NOT been `defined.
`endifThe exact inverse of \ifdef. The canonical use case is the include-guard pattern (§5 of the 7.1 `` include `` page) — \ifndef HEADER_GUARD ... `define HEADER_GUARD ... `endif`.
2.3 `else — the alternate branch
`ifdef FAST_MODE
parameter integer LATENCY = 1;
`else
parameter integer LATENCY = 4;
`endifIf FAST_MODE is defined, LATENCY becomes 1; otherwise 4. \elseprovides the "what if the condition is false" branch. Pairs with both`ifdefand`ifndef`.
2.4 `elsif — multi-way branching
`ifdef VENDOR_XILINX
`include "xilinx_primitives.vh"
`elsif VENDOR_ALTERA
`include "altera_primitives.vh"
`elsif VENDOR_LATTICE
`include "lattice_primitives.vh"
`else
`include "generic_primitives.vh"
`endifThe cascade evaluates top to bottom: the first defined macro wins; subsequent branches are skipped. \elsifis functionally equivalent to a nested`else `ifdef ... `endif `endif` but reads cleaner.
2.5 `endif — close the block
`ifdef MACRO
// ...
`endif // <-- mandatory; without it, preprocessor errors outEvery \ifdef/`ifndefblock must be terminated by a matching`endif. Nested blocks each need their own closing `endif. The preprocessor uses depth-counting, not name-matching — `endifdoesn't carry a macro name (unlike C's#endif // MACRO` annotation comment).
3. Mental Model
4. The Synthesis-vs-Simulation Discipline
The most common pattern. Simulation code that doesn't belong in the netlist gets gated on a SIMULATION macro.
module cache_controller (
input wire clk,
input wire [31:0] addr,
input wire rd_en,
output reg [31:0] rdata,
output reg hit
);
// ... cache logic ...
`ifdef SIMULATION
// Debug instrumentation — present in sim, absent in synth.
always @(posedge clk) begin
if (rd_en) begin
$display("[t=%0t] cache read addr=%h hit=%b rdata=%h",
$time, addr, hit, rdata);
end
end
// Internal assertions — runtime checks, not in synth output.
always @(posedge clk) begin
if (hit && (rdata === 32'hxxxxxxxx)) begin
$error("cache hit returned X — uninitialised line %h", addr);
$finish;
end
end
`endif
endmoduleIn a simulation build the engineer runs simulator +define+SIMULATION my_design.v. In a synthesis build the macro isn't defined, the preprocessor strips both blocks, and the synthesiser sees a module containing only the cache logic. No $display, no $error, no surprise side effects on netlist generation.
The discipline applies to:
$display/$monitor/$write— all system tasks that emit text.$assertionblocks — runtime assertions for verification only.- Behavioural reference models — golden-reference logic that runs alongside the DUT during simulation but isn't part of the chip.
- Coverage instrumentation — counters and flags that drive coverage but don't appear in synthesis output.
- Initial blocks for testbench wiring —
initial begin clk = 0; ... endstyle code that drives stimulus.
Some teams use a more specific macro for the variant — SYNTHESIS defined in synth builds, SIMULATION in sim builds, so the discipline reads \ifndef SYNTHESIS` (include in everything except synth). Both conventions work; pick one and stick to it.
5. The RTL-Variant Pattern
A single RTL file can carry multiple variants — fast vs slow, 32-bit vs 64-bit, with-feature-X vs without — gated by feature-flag macros.
module alu (
input wire [31:0] a, b,
input wire [3:0] op,
output reg [31:0] result
);
`ifdef FAST_ALU
// Single-cycle 32x32 multiplier — large area, fast timing.
reg [63:0] product;
always @(*) begin
case (op)
4'h0: result = a + b;
4'h1: result = a - b;
4'h2: begin
product = a * b;
result = product[31:0];
end
default: result = 32'h0;
endcase
end
`else
// Iterative shift-add multiplier — small area, slow timing.
// Provides the same interface but takes 32 cycles for multiply.
// ... iterative logic ...
`endif
endmoduleThe same alu module compiles to two different netlists depending on the build target. Production silicon picks the variant that fits the area / timing budget; the source code stays in one file.
The pattern generalises: gate optional features (\ifdef CACHE_PRESENT), per-customer cuts (`ifdef CUSTOMER_ACME), debug-build extra logic (`ifdef DEBUG_BUILD), even target-clock-frequency cuts (`ifdef CLK_FAST`).
6. Command-Line Definition — +define+
Simulators and synthesis tools accept +define+MACRO on the command line to define macros without modifying source code.
# Simulation build with debug instrumentation
vcs -sverilog -full64 \
+define+SIMULATION \
+define+DEBUG_VERBOSE \
my_design.v
# Synthesis build with fast-ALU variant
dc_shell -f synth.tcl \
+define+FAST_ALU \
+define+VENDOR_TSMC_N7
# Synthesis build with default variant + customer cut
dc_shell -f synth.tcl \
+define+CUSTOMER_ACME \
+define+VENDOR_TSMC_N7Three different builds, same source. The macros never touch the source files — only the build scripts decide which branches survive. This is the canonical way to manage multiple build targets without source-file proliferation.
Tool-specific syntax varies slightly:
| Tool | Define-macro flag |
|---|---|
| Synopsys VCS | +define+MACRO or +define+MACRO=VALUE |
| Cadence Xcelium | +define+MACRO or -define MACRO |
| Mentor Questa | +define+MACRO |
| Synopsys Design Compiler | set hdlin_define_macros { MACRO } (Tcl) |
| Cadence Genus | set_db hdl_macros { MACRO } (Tcl) |
| Xilinx Vivado | set_property verilog_define MACRO [current_fileset] (Tcl) |
| Verilator | +define+MACRO or -DMACRO |
Most simulators accept the +define+ form; synthesis tools generally use Tcl commands. The semantic is the same — define a macro that source-level \ifdef` will see.
7. Visual Explanation
Three figures cover the conditional-compilation model.
7.1 Visual A — preprocessor branch evaluation
The conceptual picture: the preprocessor evaluates \ifdef` against the macro symbol table; only the surviving branch reaches the compiler.
Conditional compilation — preprocessor branch evaluation
data flow7.2 Visual B — multi-way \elsif` cascade
How \elsif` evaluates top-to-bottom and short-circuits on the first match.
7.3 Visual C — build-target matrix
Three macros × three build targets shows how the same source file produces different netlists / sim binaries.
8. Nested `ifdef — Compound Conditions
Verilog has no native \ifdef A && B` syntax. Compound conditions are expressed via nesting.
// "Include debug code only if SIMULATION AND DEBUG_VERBOSE are both defined"
`ifdef SIMULATION
`ifdef DEBUG_VERBOSE
$display("[verbose] state=%s value=%h", state_name, value);
`endif
`endif
// "Include for either FAST_ALU OR FAST_FPU (but not both)"
`ifdef FAST_ALU
parameter integer PIPELINE_STAGES = 1;
`elsif FAST_FPU
parameter integer PIPELINE_STAGES = 1;
`else
parameter integer PIPELINE_STAGES = 3;
`endif
// "Include if SIMULATION AND NOT SYNTHESIS"
`ifdef SIMULATION
`ifndef SYNTHESIS
$display("sim-only, not synthesis");
`endif
`endifThe compound-condition rules:
- AND — nest one
\ifdef` inside another. - OR — use
\elsif` (with identical body in each branch) or guard each branch separately. - NOT — use
\ifndef`. - Complex boolean expressions — not directly expressible; refactor into intermediate
\definemacros set by an upstream`ifdef` cascade.
The lack of && / || is intentional — keeps the preprocessor's evaluation model simple. If you need rich boolean logic, the code is probably trying to do too much at the preprocessor layer.
9. The `default_nettype Companion Pattern
A common Chapter 7 idiom: gate \default_nettype none` on a macro so files behave strictly in lint / sim but tolerantly in legacy synth tools.
`ifndef LEGACY_TOOL
`default_nettype none
`endif
module my_module (input wire clk, ...);
// ...
endmodule
`ifndef LEGACY_TOOL
`default_nettype wire // restore default for downstream files
`endifIn modern simulators and lint tools (where LEGACY_TOOL isn't defined), \default_nettype none` catches typo'd net declarations. In a legacy synth flow where the macro is defined, the strict mode is skipped. The pattern lets one source file work across both flows.
10. Industry Use Cases
Five patterns where conditional compilation shows up in production.
10.1 Sim-only debug instrumentation
Every non-trivial RTL module has $display lines that catch regressions. Gating them on SIMULATION keeps them in sim builds and out of synth.
10.2 RTL variants for area / timing trade-offs
Fast-vs-slow ALUs, with-vs-without-cache, 32-bit-vs-64-bit datapaths. The same source supports multiple silicon variants by toggling macros.
10.3 Vendor / target-platform branches
Xilinx vs Altera vs Lattice vs ASIC primitives. The same RTL targets multiple FPGA families and an ASIC flow via \elsifcascade onVENDOR_*` macros.
10.4 Per-customer customisation cuts
Customer-specific feature gates (extra status bits, alternate register-map layouts, regulatory-compliance cuts). The same source ships to multiple customers via different \ifdef CUSTOMER_*` definitions.
10.5 Synthesis vs simulation primitive selection
`ifdef SYNTHESIS
// Vendor-specific primitive — DSP block, BRAM macro, etc.
BRAM_MACRO #(.WIDTH(32), .DEPTH(1024)) u_mem ( ... );
`else
// Behavioural model — runs in simulation, no vendor dep.
reg [31:0] mem [0:1023];
always @(posedge clk) ...
`endifThe synthesis path uses the vendor-specific primitive (which simulates correctly only with the vendor's simulation library); the simulation path uses a portable behavioural model. The RTL stays portable across simulators while still synthesising to the optimal vendor mapping.
11. Common Mistakes
Six pitfalls that catch every author at least once.
11.1 Forgotten `endif
`ifdef DEBUG
$display("debug");
// Forgotten `endif here
module other_module (...);
// ... preprocessor still inside the `ifdef block
endmoduleThe preprocessor walks past module other_module thinking it's still inside the \ifdef. If DEBUGis defined, everything works; if it isn't,module other_modulesilently disappears. The fix: every`ifdef/`ifndefmust close with a matching`endif`.
11.2 Asymmetric branches in \ifdef / \else
`ifdef VARIANT_A
reg [7:0] data;
reg [3:0] addr;
`else
reg [15:0] data;
// forgot to declare addr — only in VARIANT_A path!
`endifThe non-VARIANT_A build has no addr declaration; downstream references trigger undefined-net errors. Asymmetric branches are silently legal — the preprocessor doesn't enforce parity. Discipline: every \ifdef/`else` pair declares the same set of symbols, even if the values differ.
11.3 Macro defined in one file, referenced in another (build order matters)
// file A.v
`define MY_FEATURE
// file B.v — references `MY_FEATURE
`ifdef MY_FEATURE
// ...
`endifIf the simulator compiles B.v before A.v, MY_FEATURE isn't defined yet when B.v's \ifdefevaluates. The macro might exist after the build completes, but the`ifdefalready evaluated false. The fix: project-wide macros go in a header file`included from every file (the 7.1 `includepage pattern), not scattered`define` lines.
11.4 Same macro name used for unrelated purposes
// One library defines DEBUG for its debug build.
// Another library defines DEBUG for its assertion control.
// The user `+define+DEBUG accidentally enables both.The preprocessor doesn't scope macros by file or by team. A DEBUG defined for one library leaks into every other library's \ifdef DEBUG. The fix: prefix macros by project / library (MYPROJ_DEBUG, THEIRLIB_DEBUG) — same naming discipline as the 7.2 `define page recommends.
11.5 \elseafter`elsif` cascade
`ifdef A
// branch 1
`elsif B
// branch 2
`elsif C
// branch 3
`else
// branch 4 — fires only if A, B, C are ALL undefined
`endifThis is correct — but engineers sometimes write \elsif Bafter the`else, which is a syntax error. The cascade must be `ifdef / `elsif / `elsif / ... / `else / `endif— the`else` is always last (if present at all).
11.6 Cutting a port list mid-comma
module my_module (
input wire clk,
`ifdef HAS_RESET
input wire rst_n
`else
input wire en
`endif
);The port list is clk, rst_n if HAS_RESET is defined and clk, en if not. Missing the comma after rst_n means the next port (any port outside the \ifdef`) silently joins with no separator. Worse: if the cut crosses a comma, one build has a trailing comma and another doesn't, producing different parse-tree shapes. Discipline: place the conditional cut at a clean boundary (whole port group, whole declaration), not mid-comma or mid-declaration.
12. Debugging Lab
Three conditional-compilation debug post-mortems
13. Coding Guidelines
Six rules that teams converge on.
- Gate every non-synthesisable construct.
$display,$monitor,$write,$assertion, behavioural reference models, testbench-onlyinitial beginblocks — all wrapped in\ifdef SIMULATION` (or the project's equivalent macro). - Prefix project / library macros.
OURPROJ_DEBUG,MYLIB_FEATURE_X,RISCV_OP_LUI. Global macros without a prefix will eventually collide. - Define project-wide macros in a shared header. Put the canonical
\definelines in aproject_defines.vhthat every source file`includes. Don't rely on the command-line+define+` for project-wide configuration. - Use
+define+for build-target switches. Sim-vs-synth, fast-vs-slow, customer-A-vs-customer-B — these come from the build script, not the source file. The source documents which macros it responds to; the build script supplies the values. - Place conditional cuts at clean boundaries. Whole declarations, whole
\always` blocks, whole modules — not mid-comma, mid-port-list, mid-statement. The preprocessor doesn't care; the maintainer does. - **Document every
\ifdefmacro at its first reference.** A one-line comment naming what the macro means and where it's defined:// SIMULATION — defined by simulator command line; gates debug $display lines.` Saves the next engineer's grep.
14. Interview Q&A
15. Exercises
Three exercises that turn conditional compilation into reflex.
Exercise 1 — Identify the build target
For each +define+ invocation, predict what the build produces.
| # | Command line | Build target |
|---|---|---|
| 1 | vcs my_design.v (no defines) | ? |
| 2 | vcs +define+SIMULATION my_design.v | ? |
| 3 | dc_shell +define+FAST_ALU +define+VENDOR_TSMC_N7 | ? |
| 4 | vcs +define+SIMULATION +define+DEBUG_VERBOSE my_design.v | ? |
| 5 | dc_shell +define+CUSTOMER_ACME +define+VENDOR_XILINX +define+FAST_ALU | ? |
Hints. Map each macro to the gated code it enables. Some builds enable multiple variants simultaneously.
Exercise 2 — Spot the pitfall
Each snippet has at least one bug. Identify and fix.
// Snippet A
`ifdef HAS_RESET
input wire rst_n
`else
input wire en
`endif
); // (preceded by other ports in the port list)
// Snippet B
`ifdef A
parameter WIDTH = 8;
`elsif B
parameter WIDTH = 16;
`else
parameter WIDTH = 32;
// forgot something here
// Snippet C
`ifdef DEBUG
$display("debug: %h", state);
`elsif RELEASE
$display("release: %h", state);
`endif
// Both branches use $display — neither is synthesisable. Both should be gated on SIMULATION too.What to produce. For each, name the pitfall (from §11) and write the corrected version.
Exercise 3 — Design a build matrix
A 32-bit RISC-V CPU supports three configurations: base (RV32I only), standard (RV32IM, with multiply), and embedded (RV32IE, with reduced register file). Each can be built for simulation (with debug $display) or synthesis (no debug).
Design the macro set and write the \ifdef` cascade that gates:
- Multiply unit (
mul_unit.v) — included in standard and embedded. - 16-register register file vs 32-register register file — embedded uses 16, others use 32.
- Debug
$displaylines — included in simulation builds only.
What to produce. Name the macros, write the gating cascade in cpu_top.v, and list the +define+ flags for each of the six combinations (3 configs × 2 build modes).
16. Summary
Conditional compilation is Verilog's preprocessor-layer build-target gating mechanism. The five directives — \ifdef, `ifndef, `else, `elsif, `endif` — let one source file carry multiple build variants by stripping the dead branches before compilation.
The directive family:
- **
\ifdef MACRO** — include text ifMACRO` is defined. - **
\ifndef MACRO** — include text ifMACRO` is NOT defined. \else` — alternate branch for the inverse condition.\elsif MACRO` — additional condition in a top-to-bottom cascade.- **
\endif** — mandatory close for every`ifdef/`ifndef` block.
The five canonical use cases:
- Sim-only debug instrumentation —
$display,$monitor,$assertion, behavioural reference models gated onSIMULATION. - RTL variants for area / timing — fast-vs-slow ALUs, with-vs-without-cache.
- Target-platform branches — Xilinx / Altera / Lattice / ASIC primitive selection.
- Customer customisation cuts — per-customer feature gates.
- Synthesis-vs-simulation primitive selection — vendor hard macro in synth, portable behavioural model in sim.
The discipline:
- Gate every non-synthesisable construct — every
$display, every$assertion, every behavioural reference model lives behind aSIMULATIONmacro. - Prefix project / library macros —
OURPROJ_DEBUG, not bareDEBUG. Same naming discipline as the 7.2\define` page. - Project-wide macros live in a shared header —
project_defines.vhincluded by every source file; not scattered\definelines across files (the 7.1`include` page pattern). - Build-target switches come from
+define+— the build script decides; the source documents which macros it responds to. - Cuts at clean boundaries — whole declarations, whole
\always` blocks; not mid-comma or mid-port-list.
The compound-condition rules:
- AND — nest
\ifdefinside`ifdef`. - OR — use
\elsif` cascade or multiple guard blocks. - NOT — use
\ifndef`. - No native
&&/||— Verilog deliberately keeps the preprocessor's evaluation model simple.
This closes Chapter 7 — the four compiler-directives sub-pages (\include, `define, `timescale`, conditional compilation) together cover the preprocessor layer that every working Verilog file ultimately rides on.
The next chapter pivots from preprocessor-layer mechanism to runtime instrumentation: Chapter 8 — System Tasks & Functions covers $display, $monitor, $random, $readmemh, $finish, $stop, and the rest of the system-task vocabulary that simulator-driven verification leans on. After Chapter 8, Chapter 9 closes the foundation arc with the module / testbench assembly pattern.
Related Tutorials
- [
include](/verilog/include-directive) — Chapter 7.1; the file-inclusion mechanism that propagates`define`-d macros across the project. - `define — Chapter 7.2; the text-substitution macro mechanism that conditional compilation switches over.
- `timescale — Chapter 7.3; the simulator-time-unit / precision contract.
- Compiler Directives — Chapter 7; the parent overview of the directive family.
- parameter — Chapter 6.1; the language-layer per-instance configuration counterpart.