Part 1 · Language Foundations · Intermediate

Compilation Units & Compile Order

$unit, single-unit vs separate compilation, ordering rules, and fixing the classic undefined-type errors.

What a compilation unit is

A compilation unit is the set of source files the compiler processes together as one scope-resolution group. Declarations made at file level, outside any module or package, land in the compilation-unit scope — referred to as $unit. The LRM deliberately leaves the grouping policy to the tool: some simulators treat every file passed on one command line as a single unit (single-unit mode), others treat each file as its own unit (separate or multi-file unit mode). The same source tree can compile cleanly in one mode and fail with undefined types in the other — which is why relying on $unit is the most portable mistake in SystemVerilog . A typedef at the top of file A is visible to file B in single-unit mode and invisible in separate mode.

diagram
SINGLE-UNIT vs SEPARATE COMPILATION

  vlog a.sv b.sv c.sv            vlog a.sv ; vlog b.sv ; vlog c.sv
  (one compilation unit)         (three compilation units)

  ┌──────────────────────┐      ┌──────┐ ┌──────┐ ┌──────┐
  │ $unit (shared)       │      │$unit │ │$unit │ │$unit │
  │  typedef in a.sv     │      │ a.sv │ │ b.sv │ │ c.sv │
  │  visible to b.sv    │      └──────┘ └──────┘ └──────┘
  │  and c.sv           │      typedef in a.sv
  └──────────────────────┘      INVISIBLE to b.sv   "undefined type"

  Packages are visible across units either way —
  that is exactly why they exist.

Packages escape this mess: once compiled, a package lives in a library visible to every later compilation regardless of unit boundaries. The portable rule is therefore simple — nothing in $unit except maybe a timescale ; every shared declaration goes in a package.


Compile order is a dependency graph

Within and across compilation commands, a definition must be compiled before its first use. A module that imports bus_pkg requires bus_pkg to have been compiled already — either earlier in the same file list or in a previous compilation into the same library. Module instantiation is more forgiving because tools defer binding to elaboration, but packages, typedefs, and classes are resolved at compile time and order is strict. Real projects encode this in the filelist (.f file): packages first, then interfaces, then RTL and testbench files.

systemverilog
// ---- WRONG ORDER: tb.sv compiled before bus_pkg.sv ----
// vlog tb.sv bus_pkg.sv
//
// tb.sv:
module tb;
  import bus_pkg::*;      // ERROR: package 'bus_pkg' not found
  txn_t t;                // ERROR: undefined type 'txn_t'
endmodule

// ---- CORRECT FILELIST (tb.f) ----
// packages first, dependency order within them:
//   src/common_pkg.sv
//   src/bus_pkg.sv          (imports common_pkg → must follow it)
//   src/bus_if.sv           (interface uses bus_pkg types)
//   src/dut.sv
//   tb/tb_pkg.sv
//   tb/tb.sv
// vlog -f tb.f

Decoding the classic error messages

  • "undefined type txn_t" — the typedef was in $unit of another unit, or its package was not compiled yet: move it into a package and fix filelist order.

  • "package bus_pkg not found" — pure compile-order problem: the package source appears after its importer in the filelist.

  • "txn_t is not compatible with txn_t" — two definitions of the same-named type in different scopes (usually `include into multiple units): one package definition fixes it.

  • works in vendor A, fails in vendor B — code depends on single-unit $unit visibility; remove all $unit declarations.


Practical compilation discipline

Treat compilation structure as part of the design. One package per coherent concern (protocol types, configuration, utilities), one filelist that lists packages before users, and zero reliance on $unit. When a project uses incremental compilation into a shared library, recompiling a package invalidates everything that imports it — keeping packages small and stable reduces rebuild churn. This is also why VIP vendors ship a single self-contained package plus a filelist stub: it slots into any compile flow without ordering surprises.

diagram
FILELIST AS DEPENDENCY GRAPH

  common_pkg.sv ──► bus_pkg.sv ──► bus_if.sv ──► dut.sv
                        │                          │
                        └────► tb_pkg.sv ──► tb_top.sv

  topological order = filelist order:
    common_pkg  bus_pkg  bus_if  dut  tb_pkg  tb_top

  cycle between packages?   compile error in every order
   restructure: extract shared names into a third package

Interview angle

  • "What is $unit?" — the compilation-unit scope for file-level declarations; its visibility is tool-mode dependent, so avoid it.

  • "Why does my type compile in one simulator and not another?" — single-unit vs separate compilation defaults differ.

  • "How do you fix package not found?" — reorder the filelist so packages compile before importers.

Key takeaways

  • $unit visibility depends on tool compilation mode — portable code declares nothing at file level.

  • Packages, typedefs, and classes resolve at compile time: definition must precede use across the filelist.

  • Maintain the filelist as a topological sort: packages, then interfaces, then modules.

  • Same-named types from `include into different units are incompatible — one package definition is the fix.

Common pitfalls

  • Sharing a typedef via $unit — works in single-unit mode, breaks the day someone compiles per-file.

  • Listing tb.sv before tb_pkg.sv in the filelist — package not found.

  • Package import cycles (a imports b imports a) — no compile order works; factor out a common package.

  • Editing a base package late in a project without expecting full-environment recompilation.