SystemVerilog · Module 16
Package Dependencies & Compilation Order
SystemVerilog package compilation order — why a package must be compiled before anything that imports it, the dependency graph as a DAG and its topological sort, circular-dependency detection and the shared-base-package fix, the export keyword and facade pattern for re-publishing nested imports, and the file-list, Makefile, and three-layer architecture that keep multi-package builds deterministic across VCS, Xcelium, and Questa.
Module 16 · Page 16.4
SystemVerilog compilation is order-dependent in a way that surprises engineers coming from C. When the compiler reads import axi_pkg::axi_beat_t, it does not defer the lookup — it queries the symbol database immediately, and if axi_pkg has not been compiled yet, the reference fails. A package is a producer; everything that imports it is a consumer; and the producer must be analysed before the consumer. This lesson closes Module 16 with the build discipline that makes multi-package projects deterministic: how to read a dependency graph, why it must be a Directed Acyclic Graph, how to break the circular dependencies that creep into large testbenches, what export does that plain import does not, and the file-list and Makefile conventions every production flow uses to encode the order once and never think about it again.
1. Engineering Problem — Why Compilation Order Exists at All
A single package and a single module never expose the problem. It appears the moment a project has more than one package and one of them imports another — which is to say, on every real design.
Consider the failure that costs new verification engineers an afternoon: the testbench compiles cleanly on Monday, a colleague adds a new package on Tuesday, and the build now fails with "axi_beat_t: identifier not found" — pointing at a file that has not changed. Nothing is wrong with the file. The new package was simply added below its consumer in the file list, so the consumer was analysed first and the type did not yet exist in the symbol database.
The root cause is that package and import are semantic constructs, not textual ones. This is the distinction that everything else in this lesson follows from:
`include "axi_defs.svh"is a text paste. The preprocessor splices the file's characters in at the include site before the compiler sees anything. Include order is forgiving because the text is physically present once pasted.import axi_pkg::*is a database query. The compiler has already turnedaxi_pkginto a set of symbols (or it has not). The import resolves against whatever is in the database at that instant. Order is unforgiving because the symbols must already exist.
Get this wrong and the symptom is almost always the same cryptic "identifier not found" / "package not found" error, far from the real cause. The fix is never in the file the tool points at — it is in the order the files were handed to the tool. The rest of this lesson is about making that order correct by construction.
2. Mental Model — Compile the Producer Before the Consumer
The picture every engineer carries:
A package is a producer of symbols; anything that imports it is a consumer. The producer must be fully compiled before the consumer is analysed. When package B imports package A, B is a consumer of A — so A compiles first. When package C imports B, the requirement is transitive: A → B → C. The whole chain propagates. The dependency graph this defines must be acyclic; a cycle means "each one must come first," which is impossible.
Four invariants this picture preserves:
- The
::operator is not a loophole. Writingaxi_pkg::axi_beat_twithout animportstill queries the same symbol database, soaxi_pkgmust still be compiled first. Scope resolution changes how you name a symbol, not when its package must exist. - Transitivity is total. If a module imports
tb_env_pkg, andtb_env_pkgimportsaxi_pkg, andaxi_pkgimportsbase_types_pkg, thenbase_types_pkgmust precede all three. Every edge in the chain is a hard ordering constraint. - The graph must be a DAG. A valid compilation order is exactly a topological sort of the dependency graph. A topological sort exists if and only if the graph has no directed cycle. So "is my project compilable in some order?" is the same question as "is my dependency graph acyclic?"
includeorder andimportorder are different problems. A`includecan appear anywhere because it is resolved textually. A package reference cannot, because it is resolved semantically. Mixing the two mental models is the source of most order bugs.
The discipline that follows is mechanical: draw the graph, sort it topologically, write the file list in that order, and keep one package per file so the graph stays legible.
3. Building the Dependency Graph
The first concrete step in managing compilation order is drawing the dependency graph. Each node is a compilation unit — a package, interface, or module file. Each directed edge X → Y means "X depends on Y" (X imports from Y, so Y compiles first). Group the nodes into layers: layer 0 has no dependencies, layer N depends only on layers 0…N−1. Compiling all of layer N before any of layer N+1 is always a valid order, and the layered view makes the correct order obvious at a glance.
A valid compile order is any topological sort of this DAG — for example: base_types_pkg → math_utils_pkg → axi_cfg_pkg → apb_cfg_pkg → axi_types_pkg → apb_types_pkg → tb_env_pkg → axi_slave → apb_master → tb_top. Multiple orders are valid; anything that respects every dependency edge works.
The code below realises the first three layers of that graph. Read the imports as the edges: each import line is a "this must already be compiled" constraint.
// ── Layer 0: base_types_pkg.sv — no dependencies ────────────────────
package base_types_pkg;
typedef logic [7:0] byte_t;
typedef logic [31:0] word_t;
typedef logic [63:0] dword_t;
endpackage : base_types_pkg
// ── Layer 1: axi_cfg_pkg.sv — depends on base_types_pkg ─────────────
package axi_cfg_pkg;
import base_types_pkg::*; // Layer 0 must already be compiled
parameter int DATA_W = 64;
parameter int ADDR_W = 40;
localparam int STRB_W = DATA_W / 8;
endpackage : axi_cfg_pkg
// ── Layer 2: axi_types_pkg.sv — depends on axi_cfg_pkg + base ───────
package axi_types_pkg;
import axi_cfg_pkg::*; // Layer 1 must already be compiled
import base_types_pkg::*; // Layer 0 — also needed for byte_t
typedef struct packed {
logic [ADDR_W-1:0] addr;
logic [DATA_W-1:0] data;
logic [STRB_W-1:0] strb;
byte_t id;
} axi_beat_t;
endpackage : axi_types_pkg
// ── Layer 3: tb_env_pkg.sv — depends on the type packages ───────────
package tb_env_pkg;
import axi_types_pkg::*; // Layer 2 must already be compiled
import apb_types_pkg::*; // also Layer 2
class EnvConfig;
int n_axi_agents = 2;
int n_apb_agents = 4;
endclass
endpackage : tb_env_pkg4. Circular Dependencies — Detection and Resolution
A circular dependency occurs when package A depends (directly or transitively) on package B, and B also depends on A. Neither can be compiled first, so it is a hard error — there is no -scu flag, no reordering, and no tool option that fixes it. The graph must be restructured.
// ── PROBLEM: driver_pkg and monitor_pkg import each other ───────────
package driver_pkg;
import monitor_pkg::resp_t; // driver needs resp_t to check acks
typedef struct packed { logic [31:0] data; } req_t;
endpackage
package monitor_pkg;
import driver_pkg::req_t; // monitor needs req_t to sample
typedef struct packed { logic [1:0] status; } resp_t;
endpackage
// COMPILE ERROR: to compile driver_pkg, monitor_pkg must exist, and
// vice-versa. The cycle has no valid topological sort.The cycle is always a symptom of the same underlying fact: the two packages share data that belongs to neither of them exclusively. req_t and resp_t are transaction types that both the driver and the monitor need. They do not belong in driver_pkg or monitor_pkg — they belong in a common ancestor. Extracting them into a base package that imports nothing breaks the cycle and restores the DAG.
// ── SOLUTION (preferred): extract shared types into a base package ──
package txn_types_pkg; // new leaf — depends on nothing
typedef struct packed { logic [31:0] data; } req_t;
typedef struct packed { logic [1:0] status; } resp_t;
endpackage : txn_types_pkg
package driver_pkg; // now depends only on txn_types_pkg
import txn_types_pkg::*;
// driver-specific items here...
endpackage : driver_pkg
package monitor_pkg; // also depends only on txn_types_pkg
import txn_types_pkg::*;
// monitor-specific items here...
endpackage : monitor_pkg
// Compile order: txn_types_pkg → driver_pkg → monitor_pkg
// (the last two are now independent — either order works)A second, blunter option exists when the two packages are so tightly coupled that there is no clean split: merge them into one package. A single package cannot have a cycle with itself, so the problem disappears — at the cost of a package that may grow uncomfortably large. Use the base-package split for anything that will scale; reserve the merge for genuinely inseparable pairs.
To detect cycles in a codebase you did not write: draw the import graph and look for a back-edge — any path that returns to a node it already visited. Tools help (vcs -ntb_opts, dependency linters, or a quick grep import script that builds the edge list), but the conceptual move is always the same — find the cycle, identify the shared data, lift it into a common ancestor.
5. The export Keyword — Re-Publishing Nested Imports
Section 16.2 established the rule that trips up every engineer once: a nested import is not transitive. If package mid_pkg imports base_pkg, the names from base_pkg are visible inside mid_pkg only — a module that imports mid_pkg does not automatically see base_pkg's names. Each consumer that needs base_pkg must import it directly.
The export keyword (IEEE 1800-2017 §26.6) is the deliberate exception. It lets a package re-publish names it imported, so that consumers of the outer package see the inner package's names through it.
// ── Without export — the consumer must import base_pkg separately ───
package mid_pkg;
import base_pkg::*; // base_pkg names visible INSIDE mid_pkg only
endpackage
module top;
import mid_pkg::*;
byte_t x; // ERROR: byte_t not visible here
endmodule // fix: also 'import base_pkg::*;' in top
// ── With export — the consumer sees base_pkg names through mid_pkg ──
package mid_pkg;
import base_pkg::*;
export base_pkg::*; // re-publish base_pkg names to consumers
endpackage
module top;
import mid_pkg::*; // single import is now enough
byte_t x; // OK: visible via mid_pkg's re-export
endmoduleThe most valuable use of export is the facade package: a single package that imports a set of sub-packages and re-exports all of them, so consumers import one name and get the whole subsystem — the SystemVerilog equivalent of a C++ "umbrella header."
// ── Sub-packages, each a leaf (no cross-dependencies) ───────────────
package axi_base_pkg;
parameter int DATA_W = 64;
typedef logic [DATA_W-1:0] data_t;
endpackage
package axi_types_pkg;
import axi_base_pkg::*;
typedef struct packed { axi_base_pkg::data_t data; logic valid; } axi_beat_t;
endpackage
package axi_utils_pkg;
import axi_base_pkg::*;
function automatic int bytes_per_beat(); return axi_base_pkg::DATA_W / 8; endfunction
endpackage
// ── Facade — consumers import ONLY this ─────────────────────────────
package axi_pkg;
import axi_base_pkg::*; export axi_base_pkg::*; // constants + types
import axi_types_pkg::*; export axi_types_pkg::*; // struct types
import axi_utils_pkg::*; export axi_utils_pkg::*; // utility functions
endpackage : axi_pkg
// ── Consumer — one import, everything available ─────────────────────
module axi_slave
import axi_pkg::*; // gets base + types + utils through the facade
(
input axi_beat_t beat, // from axi_types_pkg, re-exported
output logic ready
);
localparam int BPB = bytes_per_beat(); // from axi_utils_pkg, re-exported
localparam int DW = DATA_W; // from axi_base_pkg, re-exported
endmodule6. File List Strategies for Correct Compilation Order
Every major simulator drives compilation from a file list — conventionally a .f file (also called an flist). Files are compiled top-to-bottom in the order listed, so the file list is the topological sort, written down. Getting this file right is the practical solution to the entire problem.
# ── compile.f — packages listed BEFORE the modules that import them ──
# Layer 0: no dependencies
rtl/pkg/base_types_pkg.sv
rtl/pkg/math_utils_pkg.sv
# Layer 1: depend on Layer 0
rtl/pkg/axi_cfg_pkg.sv
rtl/pkg/apb_cfg_pkg.sv
# Layer 2: depend on Layers 0–1
rtl/pkg/axi_types_pkg.sv
rtl/pkg/apb_types_pkg.sv
# Layer 3: facade / environment packages
rtl/pkg/axi_pkg.sv
tb/pkg/tb_env_pkg.sv
# Interfaces (may use package types in their modports)
rtl/if/axi_if.sv
rtl/if/apb_if.sv
# RTL modules, then testbench — always after the packages they import
rtl/src/axi_slave.sv
rtl/src/apb_master.sv
tb/src/tb_top.svThe same file list drives every major tool. Nested .f files let a master list include ordered sub-lists, which keeps the package order in one place and reuses it across flows:
# ── Invoking each simulator with the ordered file list ──────────────
vcs -sverilog -f compile.f -top tb_top -o simv # Synopsys VCS
xrun -sv -f compile.f -top tb_top # Cadence Xcelium
vlog -sv -f compile.f # Siemens Questa (then: vsim tb_top)
iverilog -g2012 -f compile.f -o sim.out # Icarus (open-source)
# ── Nested .f files — one master list includes ordered sub-lists ────
# master.f:
-f rtl/pkg/pkg_files.f # all packages, already in dependency order
-f rtl/src/rtl_files.f # all RTL modules
-f tb/tb_files.f # testbench filesFor incremental builds, a Makefile encodes each package's upstream dependencies explicitly, so changing one package only recompiles what genuinely depends on it:
# ── Makefile: incremental compilation with explicit pkg dependencies ─
TOOL := vcs -sverilog
WORKDIR := work
$(WORKDIR)/base_types_pkg.done : rtl/pkg/base_types_pkg.sv
$(TOOL) $< -work $(WORKDIR) && touch $@
$(WORKDIR)/axi_cfg_pkg.done : rtl/pkg/axi_cfg_pkg.sv \
$(WORKDIR)/base_types_pkg.done
$(TOOL) $< -work $(WORKDIR) && touch $@
$(WORKDIR)/axi_types_pkg.done : rtl/pkg/axi_types_pkg.sv \
$(WORKDIR)/axi_cfg_pkg.done \
$(WORKDIR)/base_types_pkg.done
$(TOOL) $< -work $(WORKDIR) && touch $@
# Changing axi_cfg_pkg.sv re-compiles axi_types_pkg (and its dependents)
# but NOT base_types_pkg or unrelated packages.7. Multi-Unit vs Single-Unit Compilation
Tools differ in how they treat a set of files. Knowing the mode you are in explains why an "identifier not found" error can appear even when every file is on the list.
| Mode | How it works | Order requirement | Tool example |
|---|---|---|---|
| Multi-compilation-unit (default) | Each file is its own compilation unit. Items in one file are invisible to another except via import or `include | Strict — packages must appear before their consumers in the list | VCS, Xcelium, Questa (default) |
| Single-compilation-unit (SCU) | All files are treated as one unit; the tool does a two-pass analysis that can resolve some forward references across files | Relaxed — tool resolves order internally, but producer→consumer order is still recommended | vcs -scu, xrun -scu |
| Incremental | Re-compiles only files whose content or upstream dependencies changed | Strict — dependency graph tracked per unit (Makefile or tool-managed) | Questa / Xcelium incremental |
8. Dependency Patterns in Large Designs
Large projects converge on a three-layer package architecture. Following it keeps the graph acyclic by construction, because the rule "arrows point down only, never sideways" structurally forbids the cycles of §4.
- Layer A — Foundation. Primitive typedefs (
byte_t,word_t) and pure functions (clog2,crc,parity). These import nothing — they are the leaf nodes of the DAG and compile first. - Layer B — Protocol / IP-specific.
axi4_pkg,apb_pkg,spi_pkg. Each imports only Layer A. Two Layer-B packages never import each other — if they need to share a type, that type belongs in Layer A. - Layer C — Environment / testbench. Agent classes, scoreboards, the env package. Each imports only Layer B (and Layer A indirectly).
The "never sideways between same-layer packages" rule is the one that prevents circular dependencies. The instant two protocol packages want to share a type, the architecture tells you exactly where it goes: down a layer, into the foundation.
# ── project.f — a complete ordered file list for a multi-protocol SoC ─
# ===== LAYER A: foundation packages — no dependencies =====
rtl/pkg/foundation/base_types_pkg.sv
rtl/pkg/foundation/math_pkg.sv
rtl/pkg/foundation/encoding_pkg.sv
# ===== LAYER B: protocol-specific packages (import Layer A) =====
rtl/pkg/protocol/axi4_pkg.sv
rtl/pkg/protocol/apb3_pkg.sv
rtl/pkg/protocol/spi_pkg.sv
# ===== LAYER C: testbench / environment packages (import Layer B) =====
tb/pkg/tb_axi_agent_pkg.sv
tb/pkg/tb_apb_agent_pkg.sv
tb/pkg/tb_scoreboard_pkg.sv # imports axi4_pkg, apb3_pkg
tb/pkg/tb_env_pkg.sv # imports all agent + scoreboard pkgs
# ===== Interfaces, then RTL, then testbench top =====
rtl/if/axi4_if.sv rtl/if/apb3_if.sv rtl/if/spi_if.sv
rtl/src/axi_interconnect.sv rtl/src/apb_bridge.sv rtl/src/dut_top.sv
tb/src/tb_top.svThis is precisely the layout every protocol VIP and every UVM environment is built on — the UVM treatment in Package Structure & Compilation applies this same three-layer discipline to a full agent hierarchy.
9. Debug Lab — Compilation-Order Bugs Every Engineer Hits Once
The failures that cost an afternoon the first time and five minutes every time after.
Package dependencies & compilation order — the recurring bugs
Symptom: Compile fails with "axi_types_pkg / axi_beat_t: identifier not found" — pointing at a module file that has not changed.
Cause: In the file list, the module appears above the package it imports, so the consumer is analysed before the producer's symbols exist in the database.
# ❌ compile.f — wrong order
rtl/src/axi_slave.sv # ERROR: axi_types_pkg not compiled yet
rtl/pkg/axi_types_pkg.sv # too lateFix: packages always appear above the modules that import them.
# ✅ compile.f — packages first
rtl/pkg/axi_types_pkg.sv
rtl/src/axi_slave.svGuardrail: the file list is a topological sort — leaf packages first, consumers last. When a build breaks on an unchanged file, suspect the order, not the file.
Symptom: A module imports mid_pkg and uses byte_t (which lives in base_pkg); the tool reports "byte_t: identifier not found" even though mid_pkg clearly imports base_pkg.
Cause: A nested import is not re-exported by default. base_pkg's names are visible inside mid_pkg only — not to mid_pkg's consumers.
package mid_pkg;
import base_pkg::*; // byte_t visible INSIDE mid_pkg only
endpackage // NO export — consumers don't see byte_t
module consumer;
import mid_pkg::*;
byte_t y; // ❌ ERROR unless base_pkg imported or re-exported
endmoduleFix: re-export from the middle package, or import the base package directly in the consumer.
package mid_pkg;
import base_pkg::*;
export base_pkg::*; // ✅ now consumers of mid_pkg see byte_t
endpackageGuardrail: import is local; visibility does not chain. Use the facade pattern (import + export) when you want one package to expose another's names.
Symptom: "circular dependency" or "package not found" with no single file at fault — every file looks individually correct.
Cause: A cycle hides behind an indirect path: A → B → C → A. Each edge is fine in isolation; the loop is fatal because no topological sort exists.
package A; import B::*; endpackage // A → B
package B; import C::*; endpackage // B → C
package C; import A::*; endpackage // C → A ← closes the cycleFix: extract the shared declarations into a new leaf package and have A, B, C all import it instead of each other.
package shared_pkg; /* the types A, B, C all needed */ endpackage
package A; import shared_pkg::*; endpackage
package B; import shared_pkg::*; endpackage
package C; import shared_pkg::*; endpackage // ✅ DAG restoredGuardrail: a cycle always means shared data living in the wrong place. Lift it into a common ancestor; never try to reorder your way out of a cycle.
Symptom: The build passes locally with vcs -scu but fails in CI (clean multi-unit build) with "identifier not found".
Cause: Single-compilation-unit mode resolves some forward references the file-list order got wrong. The order bug is still present — it only shows when the mode changes.
# compile.f — passes with -scu, breaks without it
rtl/src/dut.sv # uses axi_pkg
rtl/pkg/axi_pkg.sv # should have been FIRSTFix: maintain correct dependency order regardless of mode, so the file list is portable.
Guardrail: treat -scu as a performance/elaboration option, never as a way to avoid ordering your file list. The order is documentation that must survive a tool switch.
Symptom: Compile error at the export line — "cannot export an identifier that has not been imported".
Cause: export pkg::* re-publishes names the package imported. With no matching import, there is nothing to re-publish.
// ❌ COMPILE ERROR
package bad_facade;
export base_pkg::*; // never imported base_pkg
endpackageFix: import must precede export for the same package.
package good_facade;
import base_pkg::*; // import first…
export base_pkg::*; // …then re-export
endpackageGuardrail: every export pkg::* is paired with an import pkg::* above it. Think of export as "make this import visible downstream," not as a standalone statement.
Symptom: A file compiles today; a refactor reorders the packages inside it and the build breaks, or incremental compilation rebuilds far more than expected.
Cause: Two packages in one file with an internal dependency (b_pkg imports a_pkg) is legal but fragile — the dependency order is invisible in the file list and the whole file is one incremental-compile unit.
package a_pkg; /* ... */ endpackage
package b_pkg; import a_pkg::*; /* ... */ endpackage // legal but fragileFix: one package per file, filename matching the package name. The file list then is the dependency order.
Guardrail: a_pkg.sv contains only a_pkg. This keeps the graph legible and makes incremental compilation rebuild exactly what changed.
10. Q & A
The questions that come up in code review and interviews.
11. Cross-References & What's Next
This lesson covered the build discipline that makes multi-package projects deterministic: the producer-before-consumer rule, the DAG requirement and its topological sort, circular-dependency detection and the base-package fix, the export keyword and the facade pattern, and the file-list, Makefile, and three-layer conventions that encode the order once. It closes Module 16.
- Previous: 16.3 — Package-Level Parameters & Types — the shared constants and
typedeffamilies that live in the packages this lesson orders. - Module index: Module 16 — Packages — the full four-page arc from declaration to compilation order.
Related material elsewhere in the curriculum:
- import — Explicit & Wildcard — the nested-import-non-export rule that §5's
exportkeyword resolves. - Package Declaration & Usage — what a package may contain and the
::operator this lesson's ordering rules apply to. - UVM — Package Structure & Compilation — the same three-layer discipline applied to a full UVM agent hierarchy.
12. Quick Reference
// ── The fundamental rule ────────────────────────────────────────────
// A package must be compiled BEFORE every file that uses it (:: or import).
// Direct and transitive dependencies both count.
// ── Safe order = topological sort of the dependency graph ───────────
// Leaf packages (no imports) first → ... → modules / testbench last.
// A valid order exists IFF the graph is acyclic (a DAG).
// ── Circular dependency = hard error, no workaround except restructure
// Fix 1 (preferred): extract shared types into a new base package;
// both packages import the base instead of each other.
// Fix 2: merge the two packages into one (no self-cycle possible).
// typedef class forwards fix INTRA-package class cycles, not package cycles.
// ── export — re-publish nested imports (facade pattern) ─────────────
package facade;
import sub_pkg::*;
export sub_pkg::*; // consumers of facade now see sub_pkg names
endpackage
// 'export' needs a matching 'import'; it does NOT change compile order.
// ── File list ordering (.f) ─────────────────────────────────────────
// Layer 0 (leaf) packages → Layer 1 → ... → interfaces → RTL → testbench.
// One package per .sv file; filename = package name.
// Nested .f: -f pkg_files.f (before) -f rtl_files.f (before) -f tb.f
// ── Three-layer architecture (keeps the graph acyclic) ──────────────
// Layer A: foundation (base_types, math) — no imports
// Layer B: protocol (axi, apb, spi) — import only Layer A
// Layer C: env / TB (agents, scoreboard)— import only Layer B
// Rule: arrows go DOWN only; never sideways between same-layer packages.
// ── Don't ──────────────────────────────────────────────────────────
// Don't rely on -scu to mask an ordering bug — it breaks on tool switch.
// Don't put multiple packages in one file — hides order, defeats incremental.13. Summary
SystemVerilog compilation is order-dependent because package and import are semantic, not textual: a package is compiled into a symbol database, and an import resolves against whatever is already there. The governing rule is compile the producer before the consumer, applied transitively across the whole import chain. The set of constraints this defines is a dependency graph, and a valid compilation order is exactly a topological sort of that graph — which exists if and only if the graph is a DAG.
A circular dependency is the case where no topological sort exists. It is a hard error with no flag that fixes it, and it always signals shared data living in the wrong place; the canonical fix is to extract that data into a common base package both sides import. typedef class forwards resolve mutual class references inside one package — they do not break package-level cycles.
The export keyword re-publishes nested imports so consumers of an outer package see an inner package's names, enabling the facade pattern of one convenient import target — but it changes visibility, not compile order, and requires a matching import. In practice the order is written down in a file list (.f), made incremental with a dependency-aware Makefile, and kept acyclic by construction with the three-layer package architecture — foundation, protocol, environment — where imports flow strictly downward. Maintain that order explicitly even under -scu, keep one package per file, and the multi-package build that breaks for everyone else stays deterministic for you.