Part 1 · Language Foundations · Intermediate

Packages & Imports

Declaring packages, wildcard vs explicit import, the :: scope operator, export, and a protocol package of types, params, and functions.

What a package is

A package is a named, compile-once container for declarations : typedefs, parameters, functions, tasks, and classes. It contains no hardware — no instances, no always blocks, no nets driven by anything. The simulator elaborates the package a single time, and every module, interface, or class that references it sees the same definitions. This is what makes shared types possible: when two modules both use bus_pkg::txn_t, the compiler knows they are the same type, not two structurally identical strangers. Two structs defined separately by textual inclusion into different scopes are different types to the type checker, even if every field matches — a distinction that bites hard with virtual interfaces and mailbox parameterization.

systemverilog
package bus_pkg;
  // parameters: shared constants
  parameter int ADDR_W = 32;
  parameter int DATA_W = 64;

  // types: one definition, used everywhere
  typedef enum logic [1:0] { READ, WRITE, IDLE } cmd_e;

  typedef struct packed {
    cmd_e                cmd;
    logic [ADDR_W-1:0]   addr;
    logic [DATA_W-1:0]   data;
  } txn_t;

  // functions: pure utilities usable in constraints and checkers
  function automatic logic [7:0] parity_of(txn_t t);
    return ^t.data;
  endfunction
endpackage

Three ways to reference package contents

You can reach into a package three ways, in increasing order of blast radius. The scope operator bus_pkg::txn_t names one item, once, with zero namespace impact — ideal in headers and shared code. An explicit import import bus_pkg::txn_t; pulls one name into the current scope so it can be used bare. A wildcard import import bus_pkg::*; makes every name in the package a candidate — note the word candidate: a wildcard import does not actually import anything until a name is used, and a local declaration of the same name silently wins. That laziness is why wildcard imports feel harmless in small testbenches and become collision factories in large ones.

systemverilog
// 1. Scope operator — no import at all, fully qualified
module checker_m (input bus_pkg::txn_t t);
  logic [7:0] par = bus_pkg::parity_of(t);
endmodule

// 2. Explicit import — only the names you ask for
module driver_m;
  import bus_pkg::txn_t;
  import bus_pkg::cmd_e;
  txn_t current;
endmodule

// 3. Wildcard import — every name becomes visible on demand
module tb_top;
  import bus_pkg::*;
  txn_t t;                  // resolves to bus_pkg::txn_t
  cmd_e c = READ;           // enum members come along too
endmodule

export — re-publishing names

A package can export names it imported, presenting an aggregate facade. The common use is an umbrella package: vip_pkg imports and exports bus_pkg and cfg_pkg so users import one package instead of three. Without the export, names imported into vip_pkg are visible inside it but do not pass through to its importers.

systemverilog
package vip_pkg;
  import bus_pkg::*;
  export bus_pkg::*;     // re-publish: importers of vip_pkg see bus_pkg names
  import cfg_pkg::*;
  export cfg_pkg::*;

  // plus vip_pkg's own declarations...
endpackage

Choosing a reference style

diagram
REFERENCE STYLE BLAST RADIUS

  bus_pkg::txn_t          import bus_pkg::txn_t;    import bus_pkg::*;
  ───────────────         ──────────────────────    ──────────────────
  one use site            one name in scope         ALL names candidate
  zero collisions         collision = compile err   collision = SILENT
  verbose                 moderate                  terse
        │                        │                        │
        ▼                        ▼                        ▼
  shared headers,         module/class files        leaf scopes only:
  package bodies,         with few package deps     tb_top, test classes
  vendor-facing code

Interview angle

  • "Why use a package instead of `include?" — compiled namespace with type identity vs textual paste; see the include-vs-import lesson.

  • "What does import pkg::* actually do?" — makes names candidates for resolution; nothing is bound until used, and local names shadow it.

  • "What is export for?" — umbrella packages that re-publish imported names to their own importers.

Key takeaways

  • Packages hold declarations only — compiled once, giving shared type identity across the environment.

  • pkg::name has zero namespace impact; explicit import binds one name; wildcard import only makes names candidates.

  • Local declarations silently shadow wildcard-imported names — the root of most import surprises.

  • export re-publishes imported names, enabling one umbrella package per VIP.

Common pitfalls

  • Defining the same struct in two scopes via `include and expecting type compatibility — they are distinct types.

  • Wildcard-importing two packages that both define clk_period_t — works until someone references it, then ambiguity error.

  • Forgetting that enum member names (READ, WRITE) enter the namespace along with the enum type.

  • Importing inside a package without export and expecting downstream visibility — imports do not chain.