A compilation unit is the set of source files the compiler processes together as one scope-resolution group. Declarations at file level, outside any module or package, land in the compilation-unit scope called $unit.
The grouping policy belongs to the tool. Some simulators treat one command line as a single unit, others treat each file as its own unit, so a typedef at the top of file A is visible to file B in one mode and invisible in the other.
Packages escape this trap. Once compiled, a package lives in a library visible to every later compilation regardless of unit boundaries. The portable rule is short: declare nothing shared in $unit, and put every shared declaration in a package.
A definition must be compiled before its first use. A module that imports bus_pkg needs bus_pkg compiled already, either earlier in the same file list or in an earlier compilation into the same library.
Module instantiation is more forgiving because tools defer binding to elaboration. Packages, typedefs and classes resolve at compile time, so their order is strict. Real projects encode the order in a filelist: packages first, then interfaces, then RTL and testbench files.
CORRECT FILELIST (tb.f): packages first, dependency order within them
src/common_pkg.sv // lowest-level package
src/bus_pkg.sv // imports common_pkg, so it follows it
src/bus_if.sv // interface uses bus_pkg types
src/dut.sv
tb/tb_pkg.sv
tb/tb.sv // testbench top comes last// bad-example: tb.sv compiled before its package
module tb_wrong_order;
import bus_pkg::*; // package bus_pkg was never compiled: not found
txn_t t; // undefined type follows from the missing package
endmodulea_pkg imports b_pkg, and b_pkg imports a_pkg. In what order should the filelist list them?No order works: each needs the other compiled first. Move the shared names into a third package that both import.
The sketch above is the classic failure: the importer compiled before the package. Reversing the filelist order fixes it, and the checked listing below shows the healthy direction in one file.
package common_pkg;
parameter int WIDTH = 8;
typedef logic [WIDTH-1:0] data_t;
endpackage
// The package above is compiled first, so this module resolves cleanly.
module worker (
input common_pkg::data_t d,
output common_pkg::data_t q
);
assign q = d + 1'b1;
endmodule
module tb_order;
timeunit 1ns; timeprecision 1ps;
import common_pkg::*;
data_t stim, got;
worker u_w (.d(stim), .q(got));
int errors = 0;
initial begin
stim = 8'h41; #1;
if (got !== 8'h42) errors++;
stim = 8'hFF; #1;
if (got !== 8'h00) errors++;
if (errors != 0) $fatal(1, "FAIL: package import did not carry widths and types");
$display("PASS: package compiled before its importer carries widths and types");
$finish;
end
endmodulePASS: package compiled before its importer carries widths and typesFour messages cover most compilation failures. Each names a precise structural cause, and each has a precise fix.
Message | Cause | Fix |
|---|---|---|
undefined type txn_t | Typedef sat in another unit, or its package compiled later | Move the typedef into a package and fix filelist order |
package bus_pkg not found | Package source appears after its importer | List packages before their users in the filelist |
txn_t is not compatible with txn_t | Two same-named types from textual inclusion into different scopes | Keep one package definition and import it on both sides |
Works in one simulator, fails in another | Code depends on single-unit $unit visibility | Remove all shared file-level declarations |
The code shares declarations through $unit, and the two tools group files into compilation units differently. Moving the declarations into a package removes the tool dependence.
A `timescale 1ns/1ps directive sets the time unit and precision for the modules compiled after it in the same run. Moving a file changes which modules it covers, which is the same file-order trap as $unit sharing.
A timeunit and timeprecision pair declared inside a module or package travels with that source instead. Prefer the in-source form in packages, and keep bare directives out of shared headers.
// snippet: directive position matters, in-source timeunit travels with the code
`timescale 1ns/1ps // covers only modules compiled after this line
module tick (
input logic clk,
output logic done
);
timeunit 1ns; // part of this module, independent of file order
timeprecision 1ps;
// ...
assign done = 1'b0;
endmoduleThe directive covers whatever compiles after it, so adding or reordering files changes timing silently. A timeunit declaration inside each package is immune to ordering.
Keep packages small, stable and few. One package per coherent concern limits the blast radius of each edit.
Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. Icarus 13 (-g2012) runs them too, with the same output.
The wrong-order listing is a deliberately broken example and is not run.