A typedef or a small utility function that several modules in the same file need doesn't always deserve a full package. SystemVerilog lets you declare things directly at the top of a file, outside any module or package, and every module compiled alongside them can use them with no import at all. That scope has a name: $unit.

7 min read | Intermediate Level

What You'll Learn

  • What a compilation unit is, and why file grouping is tool-specific
  • How to declare something in $unit scope, and use it from any module compiled alongside it
  • Why $unit:: only disambiguates a name - it never grants forward visibility
  • The concrete ways $unit scope is not just an unnamed package

What Is a Compilation Unit?

A compilation unit is one or more source files compiled together, and exactly how files get grouped into units is tool-specific. Every compliant tool has to support both extremes, though: all files on one compile command line can form a single unit, or each file can be treated as its own separate unit.

Anything declared outside every module, interface, program, package, and class - at the outermost level of a file - lands in that file's compilation-unit scope. A file pulled in with `include becomes part of the including file's own compilation unit, not a separate one.

Declaring Something in $unit Scope

A plain declaration at the top of a file, outside any module, is enough - no special keyword marks it as "$unit scope," it's simply wherever an item ends up when it isn't nested inside anything else:


// Outside any module - this typedef and function live in $unit scope
typedef enum logic [1:0] {OK, WARN, ERROR} status_t;

function automatic string status_str(status_t s);
  return s.name();
endfunction

module tb;
  initial begin
    status_t a = OK;
    status_t b = ERROR;

    $display("a = %s", status_str(a));
    $display("b = %s", status_str(b));
  end
endmodule

Output

Output
a = OK
b = ERROR

tb uses status_t and status_str directly, with no import statement anywhere - they're simply visible, the same way a locally declared variable would be. Any other module compiled in the same unit as this file could use them exactly the same way.

Accessing It Explicitly with $unit::

Normally you never need to write $unit:: at all - a plain name already resolves to the compilation-unit-scope declaration when nothing closer shadows it. The prefix exists for the one case where something does shadow it:


int counter = 100;   // $unit-scope declaration

module tb;
  initial begin
    int counter = 5;   // this local declaration shadows the $unit-scope one

    $display("counter        = %0d", counter);
    $display("$unit::counter = %0d", $unit::counter);
  end
endmodule

Output

Output
counter        = 5
$unit::counter = 100

Name lookup always checks the nearest enclosing scope first, so the plain counter inside initial begin finds the local variable before it ever considers the one declared at the top of the file. $unit::counter skips straight past that local declaration and reaches the compilation-unit-scope one directly.

Quick Check

Q: If tb had no local counter variable at all, would a plain, unqualified counter inside its initial block still work?

Show Answer

Yes, and it would print 100. $unit:: is only needed to disambiguate a name that's shadowed by something closer. With nothing shadowing it, the plain name already resolves to the compilation-unit-scope declaration on its own.

$unit:: Doesn't Add Forward Visibility

An identifier still has to be declared earlier in the same compilation unit before it can be referenced - the $unit:: prefix only disambiguates a name that already resolves, it doesn't let you reach something declared later in the file:


int x;

module tb;
  initial begin
    x = 5 + limit;           // illegal: 'limit' isn't declared yet at this point
    x = 5 + $unit::limit;    // also illegal: $unit:: grants no forward visibility
  end
endmodule

int limit = 10;   // declared after the module that tried to use it

Moving limit's declaration above module tb is the only fix - there's no prefix or keyword that lets a reference reach forward to a declaration that hasn't happened yet in the source text.

$unit Is Not Just an Unnamed Package

$unit scope can hold anything a package can - types, parameters, functions, tasks, variables - but it isn't a package, and the differences matter:

$unit scopepackage
Has a declared nameNo - always accessed as $unitYes, its own name
Can be importedNo - there's no name to importYes, with import pkg::*; or a specific item
Visible outside its own compilation unitNoYes, from any unit that imports it

That last row is the one most worth remembering. Whether two files end up sharing one compilation unit or each gets its own is a tool and build-script decision, not something the source code controls - so relying on one file's $unit declarations being visible in another file is only safe if you know for certain they're compiled together as a single unit.

When Is This Actually Useful?

$unit scope suits a handful of small, closely related declarations shared within one file or one tightly-compiled group of files - a couple of typedefs and a helper function used only by the testbench modules in that same compile. Anything meant to be shared deliberately and portably across a whole project's separately compiled files belongs in a real SystemVerilog Package instead, which can be explicitly imported no matter how the build happens to group files.

Common Beginner Mistakes

Mistake #1: Trying to import $unit

import $unit::*; is illegal, because $unit has no declared name for an import statement to reference.

There's also no need to import it - compilation-unit-scope items are already visible everywhere within their own unit, with no import step at all.

Mistake #2: Assuming $unit:: can reach a later declaration

As the forward-reference example shows, $unit:: only disambiguates a name that's already reachable - it adds no special ability to see past the normal declare-before-use rule.

The only fix for a forward-reference error is moving the declaration earlier in the file.

Mistake #3: Assuming $unit content is visible across every file in the project

Whether separate files share one compilation unit, or each file gets its own, is a tool-specific build decision - not something guaranteed by the language itself.

Relying on a $unit declaration from one file being visible in another is fragile unless the build is known to compile them as a single unit; a real package is the portable way to share something across files on purpose.

Practice Exercise

Challenge: Predict the Shadowed Value

Given int limit = 42; declared in $unit scope, and a module with initial begin int limit = 7; $display("%0d %0d", limit, $unit::limit); end, what does the $display print?

Hint

Work out what the plain, unqualified limit resolves to first, then what the explicitly qualified one resolves to - they don't have to be the same value.

Solution

It prints 7 42. The plain limit finds the local variable declared inside the initial block first, since the nested scope is always searched before the compilation-unit scope. $unit::limit skips past that local variable and reaches the compilation-unit-scope declaration directly.

Summary

In this tutorial, you learned:

  • A compilation unit is one or more files compiled together; exactly how files are grouped into units is tool-specific
  • Anything declared outside every module, interface, program, package, and class lands in that file's compilation-unit ($unit) scope, with no import needed to use it elsewhere in the same unit
  • $unit::name explicitly reaches the compilation-unit-scope declaration, which matters only when something closer shadows that name
  • $unit:: never grants forward visibility - the identifier still has to be declared earlier in the source
  • $unit has no name of its own, so it can't be imported, and its declarations are never visible outside their own compilation unit - unlike a package, which can be imported from anywhere
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5