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
$unitscope, and use it from any module compiled alongside it - Why
$unit::only disambiguates a name - it never grants forward visibility - The concrete ways
$unitscope 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
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
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 scope | package | |
|---|---|---|
| Has a declared name | No - always accessed as $unit | Yes, its own name |
| Can be imported | No - there's no name to import | Yes, with import pkg::*; or a specific item |
| Visible outside its own compilation unit | No | Yes, 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::nameexplicitly 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$unithas 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