Part 2 · OOP for Verification · Intermediate
local, protected & Encapsulation
Access control semantics, hiding solver knobs and internal queues, accessor patterns, and encapsulation in VIP base classes.
Three visibility levels
By default every class member in SystemVerilog is public — any code holding a handle can read it, write it, or call it. Two qualifiers restrict that: local makes a member visible only inside the declaring class, and protected makes it visible inside the declaring class and its subclasses . The compiler enforces this at compile time — violations are errors, not warnings.
WHO CAN SEE WHAT
member qualifier │ same class │ subclass │ outside code
─────────────────┼────────────┼──────────┼──────────────
(none) = public │ yes │ yes │ yes
protected │ yes │ yes │ no
local │ yes │ NO │ no
class base;
local int secret; ◄── even 'class child extends base'
protected int family; cannot touch secret
int open; ◄── anyone with a handle can write open
endclassThe deeper question is why hide anything. Encapsulation is a contract: the public members are the supported interface, everything else is implementation that may change. If users can reach internals, every internal becomes API — you can never refactor it without breaking someone. Hiding internals keeps the class free to evolve and keeps invariants (a sorted queue, a count that matches queue size) enforceable, because only the class's own methods can modify the protected state.
What to hide in verification classes
Typical candidates
Solver knobs — distribution weights and internal rand modes that tests should set through documented methods, not by poking fields.
Internal queues — a scoreboard's expected queue must change only via add_expected()/check(), or its bookkeeping desynchronizes.
Bookkeeping counters — match/mismatch counts that must stay consistent with each other.
Handles to internal helpers — a driver's internal state machine object is not part of the API.
class scoreboard;
// Hidden: only this class may touch the queue and counters,
// so 'n_checked == hits + misses' can never be violated from outside.
local bus_txn expected_q[$];
local int hits, misses;
// Public API — the supported contract
function void add_expected(bus_txn t);
expected_q.push_back(t);
endfunction
function void check_actual(bus_txn t);
bus_txn exp;
if (expected_q.size() == 0) begin
$error("unexpected txn id=%0d", t.id);
misses++;
return;
end
exp = expected_q.pop_front();
if (exp.data === t.data) hits++;
else begin
misses++;
$error("mismatch id=%0d exp=%0h got=%0h", t.id, exp.data, t.data);
end
endfunction
// Accessor: read-only view of hidden state
function int get_miss_count();
return misses;
endfunction
endclassAccessor patterns
When outside code legitimately needs a hidden value, expose a getter (get_miss_count()) rather than making the field public. A getter grants read access without write access, can compute the value lazily, and gives you one place to add logging or assertions later. Add a setter only when controlled mutation is genuinely part of the API — and validate inside it.
Encapsulation in extendable VIP base classes
The local vs protected choice matters most in VIP base classes that users extend. Mark a member protected when subclasses are expected to use it — a driver base class's virtual interface handle, retry counter, or hook state. Mark it local when even subclasses must go through methods — usually true for invariant-carrying state. A common interview question: 'your VIP user extended your driver and the build broke after your update — what visibility mistake did you make?' Answer: you left implementation details public (or protected) and they became de-facto API.
class vip_driver_base;
protected virtual bus_if vif; // subclass drivers need pin access
protected int unsigned timeout; // subclasses may tune behavior
local int unsigned txn_in_flight; // internal invariant — methods only
function void set_timeout(int unsigned t);
if (t == 0) $error("timeout must be nonzero");
else timeout = t;
endfunction
protected task wait_grant();
// shared protocol step usable by every subclass driver
@(posedge vif.clk iff vif.gnt);
endtask
endclass
class my_chip_driver extends vip_driver_base;
task drive(bus_txn t);
wait_grant(); // OK: protected, subclass may call
vif.addr <= t.addr; // OK: protected vif
// txn_in_flight++; // ERROR: local in base — not visible here
endtask
endclassKey takeaways
Default visibility is public — encapsulation in SV is opt-in, so qualify deliberately.
local = this class only; protected = this class plus all subclasses.
Hide invariant-carrying state (queues, counters) and expose getters, not fields.
In VIP base classes, protected marks the intended extension surface; local protects internals.
Common pitfalls
Leaving everything public — every field becomes API and refactoring breaks users.
Using local in a base class meant to be extended — subclasses cannot reach state they need.
Writing setters with no validation — encapsulation in name only.
Assuming local prevents access from the same class via another object's handle — it does not; local is per-class, not per-object.