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.

diagram
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
  endclass

The 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.

systemverilog
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
endclass

Accessor 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.

systemverilog
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
endclass

Key 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.