Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
    • DPI-C: Calling C from SystemVerilog
    • bind: Attaching Verification to RTL
    • Macros, Defines & Compilation
      • `define & Text Macros
      • `ifdef & Conditional Compilation
      • `include & Guard Discipline
      • Debugging Macro Problems
    • Plusargs, Files & Runtime Configuration
    • Simulation Performance & TB Memory
    • RTL/TB Boundaries, Races & Timing
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Macros, Defines & Compilation
  1. Topics
  2. SystemVerilog
  3. Advanced Language & Integration
  4. Macros, Defines & Compilation
  5. Debugging Macro Problems

Part 7 · Advanced & Integration · Intermediate

Debugging Macro Problems

Reading expansion errors, loud configuration guards, a safe log macro, and build stages.

On this page

    The compiler reads the expansion

    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 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();
    1. Re-expand the macro by hand and read the expansion as plain code.

    2. Check argument text for commas or unbalanced parentheses that split the expansion.

    3. Check for the double-semicolon and dangling-else pattern inside if and else.

    4. Verify the macro is defined at the use point: definition order follows compile order.

    Silent skips and loud guards

    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

    ` ifdef` on a misspelled flag, or the +define+` dropped from one Makefile target.

    Feature missing from one build

    Different defines per tool, or a ` undef`` hiding in an included file.

    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 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.");
    // `endif

    A log macro that survives if-else

    Wrapping 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.

    design.sv
    1
    2
    3
    4
    // 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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    `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
    endmodule

    Build stages and where errors surface

    The 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.

    Diagram
    # 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

    Common pitfall

    Debugging at the error line instead of the expansion is the classic waste: the use site shows the macro name, not the broken text. Treat redefinition warnings as errors, and trust the preprocessed dump over the source file when they disagree.

    Interview

    Likely question: write the safe macro live.

    • Parenthesize the condition and pass the message through %s.

    • Consume the caller semicolon with do and while (0), and stamp ` __FILE__` and __LINE__``.

    • Find a never-compiled ` ifdef`` branch through the build log defines and a preprocessed dump.

    Check yourself ` 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.

    Check yourself A 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.

    Related topics

    • Text macros

    • Conditional compilation

    Previous

    198 / 259

    Next: Plusargs, Files & Runtime Configuration

    Up next

    Up nextPlusargs, Files & Runtime Configuration

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • `define & Text Macros
    • `ifdef & Conditional Compilation