The compiler never sees the macro. It sees the expanded text, so the error describes code never typed, reported at the line where the macro was used.
Re-expand the macro by hand and read the result as plain code. The bug below is not at the else: it is the macro design plus the caller semicolon.
// bad-example
`define CHECK(cond) if (!(cond)) $display("check failed");
// Use site: the caller's semicolon is a null statement after the body,
// so the outer if ends before the else and the else dangles.
// if (enable)
// `CHECK(a == b);
// else
// do_other();Re-expand the macro by hand and read the expansion as plain code.
Check argument text for commas or unbalanced parentheses that split the expansion.
Check for the double-semicolon and dangling-else pattern inside if and else.
Verify the macro is defined at the use point: definition order follows compile order.
The expensive macro bugs produce no error at all. A misspelled ` ifdef`` name is simply false, and a redefinition warning scrolls past while every later use takes the new body.
Symptom | Likely preprocessor cause |
|---|---|
Assertions never fire | ` |
Feature missing from one build | Different defines per tool, or a ` |
Constant has the wrong value | Macro redefined later in compile order; only a warning marks it. |
Verilator (and some other tools) accept ` error` as a language extension: it stops compilation with a message. IEEE 1800-2017 has no such directive, and Icarus reports it as an undefined macro. Portably, put an elaboration-time $fatal` in module scope under the guard. The listing below is not run in this course's checks because a passing build cannot demonstrate a compile stop.
// snippet
`ifdef FPGA_TARGET
`define TARGET_DEFINED
`endif
`ifdef ASIC_TARGET
`define TARGET_DEFINED
`endif
`ifndef TARGET_DEFINED
`error "No target selected: add -DFPGA_TARGET or -DASIC_TARGET to the compile." // tool extension
`endif
// Portable form, inside a module: an elaboration-time system task.
// `ifndef TARGET_DEFINED
// $fatal(1, "No target selected: add -DFPGA_TARGET or -DASIC_TARGET.");
// `endifWrapping a statement macro in begin and end does not absorb the caller semicolon. The ; after end is a null statement that ends the outer if, so the else still dangles.
// bad-example
// Looks safe, still dangles: if (c) begin ... end; else — the caller's
// semicolon after end is a null statement, so the else has no if.
// Shown as the mistake the do-while form below replaces.The fix is a body that consumes the semicolon: do and while with a constant condition. The caller ; completes the loop statement, so the macro is one statement in every context.
The predefined ` __FILE__` and __LINE__`` expand at the use site. They stamp every message with its true location.
`define TB_ASSERT(cond, msg, cnt) \
do begin \
if (!(cond)) begin \
$display("[TB_ASSERT] %s (%s:%0d)", msg, `__FILE__, `__LINE__); \
cnt++; \
end \
end while (0)
module tb_assert;
int err_count = 0;
int phase_done;
initial begin
`TB_ASSERT(1 == 1, "sanity true", err_count);
`TB_ASSERT(1 == 0, "first forced miss", err_count);
phase_done = 1;
if (phase_done == 1)
`TB_ASSERT(2 + 2 == 4, "math holds", err_count);
else
`TB_ASSERT(1 == 0, "wrong branch", err_count);
if (phase_done == 0)
`TB_ASSERT(1 == 0, "wrong branch", err_count);
else
`TB_ASSERT(3 + 4 == 8, "second forced miss", err_count);
if (err_count == 2) $display("PASS: do-while assert survives if-else branches and counts 2 misses");
else $fatal(1, "FAIL: assert count is wrong");
$finish;
end
endmoduleThe retired compilation-flow page survives here as one section. Compilation is per file and elaboration is whole-design, and that split tells you where to look.
Syntax lives in one file, while a missing module means the file never reached the library. Run-time failures need live values and often a seed.
Stage | Scope | Example failure |
|---|---|---|
Compile | Per file | Syntax error, unknown type, macro expansion failure. |
Elaborate | Whole design | Module not found, port width mismatch, bad parameter override. |
Optimize | Whole design | Signal stripped from waves until rebuilt with debug access. |
Simulate | Live values | Null handle, out-of-bounds index, assertion failure. |
Icarus folds the stages into one compile-and-run step, and Verilator folds them into one binary build. The staged Questa flow below is an unavailable-tool illustration: this course never runs it.
# Staged flow, Questa-style: illustration only, not run in this course.
vlib work && vlog -work work rtl/*.sv tb/*.sv
vopt tb.tb_top -o tb_top_opt -debug
vsim tb_top_opt -do "run -all"
# Open-tool equivalents used in this topic:
# iverilog -g2012 -o simv design.sv tb_top.sv && vvp simv
# verilator --binary --timing --assert --top-module tb_top tb_top.sv ifdef ASSERT_ON`` guards the assertions, but nothing ever fires and no error appears. List two checks in order.First confirm the flag reaches the compile: grep the build log for the +define+. Then dump the preprocessed output and confirm the guard region survived expansion.
begin and end wrapped assert macro is called with a trailing semicolon inside if and else. Why does the else still dangle?The caller semicolon after end is a null statement that terminates the outer if. The do and while (0) form instead consumes that semicolon as its own terminator.