Skip to content

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 turned axi_pkg into 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. Writing axi_pkg::axi_beat_t without an import still queries the same symbol database, so axi_pkg must 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, and tb_env_pkg imports axi_pkg, and axi_pkg imports base_types_pkg, then base_types_pkg must 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?"
  • include order and import order are different problems. A `include can 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.

Five dependency layers, compiled top to bottom: Layer 0 leaf packages, Layer 1 config packages, Layer 2 type packages, Layer 3 the environment package, and Layer 4 modules and interfaces compiled last.Dependency layers — compile top band firstDependency layers — compile top band firstLayer 0 — leaf packages (compile first)base_types_pkg, math_utils_pkg — no importsbase_types_pkg, math_utils_pkg — no importsLayer 1 — config packagesaxi_cfg_pkg, apb_cfg_pkg — import base_types_pkgaxi_cfg_pkg, apb_cfg_pkg — import base_types_pkgLayer 2 — type packagesaxi_types_pkg, apb_types_pkg — import Layer 0/1axi_types_pkg, apb_types_pkg — import Layer 0/1Layer 3 — environment packagetb_env_pkg — imports the Layer 2 type packagestb_env_pkg — imports the Layer 2 type packagesLayer 4 — modules / interfaces (compile last)axi_slave, apb_master, tb_topaxi_slave, apb_master, tb_top
Figure 1 — a package dependency DAG, viewed as layers. The top band compiles first; each band depends only on the bands above it. Compiling every band in order is always a valid topological sort.

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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_pkg

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endmodule

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endmodule

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ── 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.sv

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ── 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 files

For incremental builds, a Makefile encodes each package's upstream dependencies explicitly, so changing one package only recompiles what genuinely depends on it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ── 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.

ModeHow it worksOrder requirementTool example
Multi-compilation-unit (default)Each file is its own compilation unit. Items in one file are invisible to another except via import or `includeStrict — packages must appear before their consumers in the listVCS, 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 filesRelaxed — tool resolves order internally, but producer→consumer order is still recommendedvcs -scu, xrun -scu
IncrementalRe-compiles only files whose content or upstream dependencies changedStrict — 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.

Three stacked package layers: testbench packages on top depend on protocol packages, which depend on foundation packages at the bottom.Three-Layer Package ArchitectureThree-Layer Package ArchitectureLayer C — Environment / Testbenchtb_axi_pkg, tb_apb_pkg, tb_env_pkg — import only Layer Btb_axi_pkg, tb_apb_pkg, tb_env_pkg — import only Layer BLayer B — Protocol / IP-Specificaxi4_pkg, apb_pkg, spi_pkg — import only Layer Aaxi4_pkg, apb_pkg, spi_pkg — import only Layer ALayer A — Foundation (compiled first)base_types_pkg, math_pkg — no imports (leaf nodes)base_types_pkg, math_pkg — no imports (leaf nodes)
Figure 2 — the three-layer package architecture. Imports flow strictly downward; same-layer packages never import each other, which is what keeps the dependency graph acyclic.
  • 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ── 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.sv

This 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

1. Module listed before the package it imports

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ❌ compile.f — wrong order
rtl/src/axi_slave.sv         # ERROR: axi_types_pkg not compiled yet
rtl/pkg/axi_types_pkg.sv     # too late

Fix: packages always appear above the modules that import them.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ✅ compile.f — packages first
rtl/pkg/axi_types_pkg.sv
rtl/src/axi_slave.sv

Guardrail: 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.

2. Assuming a nested import is transitive

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endmodule

Fix: re-export from the middle package, or import the base package directly in the consumer.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package mid_pkg;
    import base_pkg::*;
    export base_pkg::*;        // ✅ now consumers of mid_pkg see byte_t
endpackage

Guardrail: import is local; visibility does not chain. Use the facade pattern (import + export) when you want one package to expose another's names.

3. Indirect circular dependency

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package A; import B::*; endpackage   // A → B
package B; import C::*; endpackage   // B → C
package C; import A::*; endpackage   // C → A  ← closes the cycle

Fix: extract the shared declarations into a new leaf package and have A, B, C all import it instead of each other.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 restored

Guardrail: 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.

4. Relying on -scu to mask an ordering bug

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# compile.f — passes with -scu, breaks without it
rtl/src/dut.sv               # uses axi_pkg
rtl/pkg/axi_pkg.sv           # should have been FIRST

Fix: 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.

5. export without a matching import

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ COMPILE ERROR
package bad_facade;
    export base_pkg::*;        // never imported base_pkg
endpackage

Fix: import must precede export for the same package.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package good_facade;
    import base_pkg::*;        // import first…
    export base_pkg::*;        // …then re-export
endpackage

Guardrail: 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.

6. Multiple packages in one file hides the order

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package a_pkg; /* ... */ endpackage
package b_pkg; import a_pkg::*; /* ... */ endpackage   // legal but fragile

Fix: 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.

Related material elsewhere in the curriculum:

12. Quick Reference

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet