Chapter 1 introduced Emergent Verification (EV): a verification approach in which the testbench architecture grows feature-by-feature instead of being built monolithically up front, with stimulus and checking adapting to coverage feedback during the run. EV reduces initial setup cost and lets edge-case scenarios appear naturally as the environment evolves.
That kind of adaptation needs a software substrate that supports independent, concurrent components. The remaining barriers come from tightly coupled Object-Oriented Programming as it is used in mainstream UVM environments: shared mutable state across forked threads, fragile lock disciplines, and execution flow that is hard to reason about under parallelism. This chapter introduces the actor model as the substrate that fits the EV requirements, develops the core SystemVerilog implementation, and walks through the parallel actor_pkg extensions that turn it into a production-grade framework.
Before introducing the actor model, it helps to be specific about the OOP limitations that motivate it. In a standard UVM-style environment, execution is largely synchronous and blocking. A single component runs tasks sequentially: get a sequence item, drive pins, wait for the response, and process coverage. The component’s thread stalls until the operation completes.
When the design scales, the natural OOP response is to instantiate more components and spawn more threads with fork-join, fork-join_any, or fork-join_none. Those threads share an execution space: configuration objects, sequencer arbitration state, shared memory variables.
That shared-state shape introduces the well-known concurrency hazards: data races from concurrent writes, locking with mutexes and semaphores to prevent the races, and threads idling while they wait for locks. Race conditions become hard to reproduce because thread scheduling is up to the simulator. Adding more parallel objects to shared memory does not scale linearly — past a point, it stops scaling altogether.
Concurrent, distributed systems — biological, social, mechanical — are the norm in nature, not the exception. Independent elements interact through messages. To anchor what the actor model will look like in software, consider a familiar real-world example: the kitchen of a busy vegetarian restaurant, The Concurrent Carrot.
How does a real kitchen handle the complexity of many simultaneous orders? By splitting the work across decoupled, independent roles:
• The Waiter Actor takes orders from customers and writes each one onto a ticket.
• The Chief Cook Actor (dispatcher) reads tickets from the rail and routes each ticket to an available worker.
• The Sous Chef Actors (workers) cook in parallel, each pulling work from its own queue.
Instead of employees barking blocking instructions directly at one another across the room (Imperative Method Invocation), they write the customer’s order onto an order ticket and place it sequentially on a ticket rail over the stove. That ticket is an Immutable Message (a data struct), and the ticket rail is an Asynchronous Mailbox.
If the cooks fall behind schedule, the Waiter does not pause their operations; they continue asynchronously, taking orders and placing tickets on the rail. Every worker executes entirely autonomously on their own localized data without ever locking the state of their peers.
The kitchen example shows how human-scale concurrent systems are usually organized: workers are isolated, there is no shared global execution space, and operations move between workers through localized asynchronous queues.
This is the operational shape of Message-Oriented Programming (MOP). The same shape applied to software yields the actor model, which the rest of this chapter develops as the substrate for an Emergent Verification environment.
The actor/hardware-module isomorphism is fractal: not only is the top-level module an actor, every primitive inside the module is too. Silicon is a network of billions of independent concurrent elements:
• A flip-flop is an actor. It holds local state, waits for a clock-edge event, and on that event latches its input.
• A logic gate is an actor. It evaluates its inputs, applies a propagation delay, and produces an output event.
• Wires are messages. Ports, nets, and output signals carry events between these elements.
A modern SoC is millions to billions of these elements running concurrently, each advancing its own local state by exchanging events with its neighbors. There is no shared execution; the only coupling is the wires between elements. This is the shape the actor model captures in software.
The punchline of this isomorphism is the chapter’s thesis in one sentence: because hardware is already an actor system, modeling its testbench as actors means the testbench is the same shape as the DUT. UVM forces a shape change at the DUT/testbench boundary — a hierarchy of uvm_component classes with phasing, factory overrides, and TLM ports interposed between the testbench and the modules it verifies. The actor framework does not. The same declarative typed-wiring substrate that connects two RTL modules through ports connects two verification actors through mailboxes, so verifying one with the other does not require crossing an architectural boundary.
The actor model is the formal foundation that Message-Oriented Programming has been pointing at. It treats the “actor” as the primitive of concurrent computation. Stripped of the surrounding software terminology — asynchronous message passing, location transparency — an actor maps closely onto a Finite State Machine: an isolated state vector that advances in response to inputs.
The mapping back to a hardware block is the same one already implied by the previous section:
• Input pins \(\rightarrow \) the actor’s mailbox: incoming messages queue up here until the handler is ready to process them.
• Flip-flop registers \(\rightarrow \) the actor’s private state variables.
• Combinational logic (next state) \(\rightarrow \) the actor’s message handler: it reads the current state and the incoming message and computes the new state, with no shared memory and no locks.
• Output pins \(\rightarrow \) messages the actor publishes to its downstream subscribers.
An actor is an isolated, autonomous unit that obeys three rules:
1. Private state. An actor’s state is reachable only from inside the actor. It does not share pointers or memory references to its internal variables, so concurrent actors cannot race on shared state.
2. Asynchronous message passing. The only way to interact with an actor is to send a message to its mailbox; the message is either immutable or transferred by value.
3. Reactive concurrency. On receiving a message, an actor may:
• Alter its own private state to dictate how it should respond to the next incoming message.
• Send new messages to other actors in its topology.
• Spawn entirely new, independent actors to delegate sub-tasks dynamically.
To visualize the internal structural mechanics of this model, consider the following pipeline architecture, which focuses on Actor \(a_2\). Here, \(a_2\) asynchronously receives an incoming message (msg1) from upstream Actor \(a_1\), processes it, and routes the resulting output (msg2) to downstream Actors \(a_3\) and \(a_4\).
In a UVM testbench, the driver advances by calling get_next_item on the sequencer, and the sequence blocks in finish_item until the driver signals item_done. Stimulus, response, and the next request all sit on the same call stack.
Actors break this coupling. When a1 produces an item, it does not wait for a response: it puts the message envelope (msg1) into a2’s mailbox and returns immediately to its own work.
a2 runs on its own thread. Its handler reads from the mailbox, examines the envelope’s type with msg.getTypeName(), and updates its private state accordingly. There is no shared variable between a1 and a2, so no locking is needed.
When a2 produces msg2, it does not name a3 and a4 explicitly. It calls publish(msg2), which looks up the subscribers wired for msg2’s type and writes the message into each one’s mailbox. The destination set lives in a2’s typed subscriber map, populated by external ‘WIRE declarations at construction time, not in the code that produced the message.
The topology — which actor receives which message types from which producers — is built once at construction time using ‘WIRE:
class RootTopology extends Actor;
// a1..a5 are concrete Actor subclasses (LeafActor here); Actor itself is virtual and cannot be instantiated directly.
function new();
super.new("root");
begin
LeafActor a1 = new("a1");
LeafActor a2 = new("a2");
LeafActor a3 = new("a3");
LeafActor a4 = new("a4");
LeafActor a5 = new("a5");
// Each `WIRE statement is a typed edge: producer, the message type that flows over the edge, consumer. The producer's code never references the consumer; the consumer's code never references the producer; the
topology lives here, in the parent that owns both ends.
`WIRE(a1, msg1_s, a2) // a2 wants msg1 from a1
`WIRE(a1, msg3_s, a5) // a5 wants msg3 from a1
// a2 publishes msg2 to both a3 and a4.
`WIRE(a2, msg2_s, a3)
`WIRE(a2, msg2_s, a4)
end
endfunction
endclass
The actor timeline below shows how three concurrent actors interleave over time. Each actor advances its own state in response to messages on its mailbox; dashed arrows mark the causal links between a published message and the message it eventually triggers downstream.
The shape of this timeline matches the shape of the DUT itself. The DUT is also a concurrent network of state machines exchanging events on wires; modeling its testbench as a concurrent network of state machines exchanging messages on mailboxes preserves that shape rather than collapsing it into a sequential class hierarchy. Because the actor abstraction is close to an FSM, the same testbench code is a candidate for emulation flows, where dynamic-heap-heavy UVM testbenches are not.
The framework rests on a few core principles that follow directly from this shape:
• Event-driven reactivity. Actors do not run sequential blocking sequences; each one waits on its mailbox and reacts to whatever message arrives next. This raises event-driven programming from the RTL signal level (where designers already work in this style) to the transaction level.
• Loose coupling. Actors communicate only through messages, so swapping or replacing an actor only affects components that subscribe to its messages.
• Dependency injection. An actor receives the resources it needs (configuration, virtual interfaces, downstream mailboxes) through its constructor or setters, rather than reaching out for global state. The same actor can then be exercised in isolation by injecting test stubs.
• Composition over hierarchy. New actors are added by writing a class and wiring its inputs and outputs with ‘WIRE from the parent. Because there is no central scheduler, adding actors does not introduce contention.
• Generic parameterization. The Msg#(T) envelope is parameterized on the payload type, so the same actor base class supports any struct payload without changes to the substrate.
The actor model handles control flow cleanly, but passing data between concurrent actors introduces a second issue: data races. If a CPU actor hands a Cache actor a reference to a transaction and then mutates the transaction while the Cache is still reading it, the receiver’s view of the data is undefined. Concurrent languages address this by guaranteeing that data passed between actors is either immutable or copied by value.
Translating that guarantee into SystemVerilog needs care. A SystemVerilog mailbox of class handles passes the handle, not a copy of the underlying class instance, so a publisher that mutates the underlying object after putting it on the mailbox lets the receiver see the mutation. UVM testbenches that pass class transactions through TLM ports rely on a discipline that the language does not enforce.
The actor framework solves this structurally:
Isolation between actors comes from Msg#(T) wrapping a value-type payload T. The wrapper is a class (so it can flow through the strongly typed mailbox), but its constructor copies T into the class instance, and the receiver’s unwrap returns T by value. Because T is a SystemVerilog struct (or a primitive / enum), every cross-mailbox transfer is effectively a deep copy of the payload, even though the mailbox itself only moves a class handle. The discipline that makes the model safe is therefore: all message payloads in this framework are structs (or other value types), never class handles.
This rule produces the immutability the actor model needs while staying inside what the SystemVerilog language gives us. SystemVerilog struct types and primitives like typedef enum are strictly passed by value; assigning one struct into another copies its contents. A class wrapper around a struct gives us a typed envelope that can flow through a single mailbox queue, and the value-copy semantics of the struct field do the deep-copy work for us. The architectural rule is succinct: Actors are OOP classes, but messages are exclusively value types — structs (or primitives wrapped as such).
Restricting messages to plain SystemVerilog structs moves the framework from an Object-Oriented design — where data and behavior live in the same class — toward a Data-Oriented design, where the two are kept apart.
In classic UVM, an object encapsulates both state variables (e.g. rand int data;) and behavior (pack(), unpack(), print()) inside the same class. The cohesion this gives can become a liability in verification: transaction classes accumulate behavior, the behavior is locked to the specific data shape it parses, and changing the sequence behavior cascades into many call sites.
The actor framework splits these responsibilities into two:
1. Data (the struct envelope): carries state and payload, with no behavior beyond field definitions.
2. Behavior (the actor): holds no persistent payload memory; it consumes incoming envelopes from its mailbox and produces outgoing ones.
With behavior moved out of the data class, messages flow through the topology as small, value-copied structs. Each actor’s behavior changes independently of the message shape, and the message shape can change without rippling into every call site that touches it.
A useful analogy comes from the high-performance game-engine industry. Game engines moved from heavily OOP to data-oriented designs once developers realized that cache behavior dominates performance: modern CPUs fetch memory in 64-byte cache lines, and pointer-chasing through scattered heap allocations is far slower than streaming through contiguous arrays.
UVM transactions are usually new()-allocated on the heap, so a stream of transaction objects is scattered across memory. Iterating over millions of such objects causes a cache miss on most accesses — the CPU stalls fetching the next cache line from main memory. The 64-bit pointer is small, but dereferencing it costs the cache miss (typically tens to a hundred nanoseconds).
A data-oriented design lays transactions out as contiguous arrays of structs. When an actor iterates over its mailbox of plain structs, the CPU prefetches whole cache lines at a time. There are no virtual-function-table lookups inside the loop, so each access resolves close to L1 latency. Once memory bandwidth is the bottleneck, copying small structs is faster than chasing pointers into the heap.
Beyond raw performance, passing an object handle across concurrent streams is a concurrency hazard. If a monitor parses bus traffic, populates a transaction object, and passes that handle to several observer components, the monitor still holds write access to the same object. If the monitor reuses or overwrites that object while the observers are still reading it, the observers see a torn or stale view of the transaction.
Because OOP classes hide state behind their methods, splitting a monolithic verification object across concurrent simulation threads is risky without mutex discipline. Separating form from behavior removes that risk: each actor owns its own deep-copied struct, so it is unambiguous which actor processes which payload. This is the property that lets future emulation/co-simulation tools schedule actor evaluation across multiple cores without lock contention.
Decoupling form from behavior also removes a debugging hazard. In UVM, a set_type_override() call elsewhere in the test environment can intercept a factory request and substitute a different concrete type, so the running code can diverge from the source the engineer is reading (“spooky action at a distance”). A plain SystemVerilog struct has no virtual dispatch, so the payload cannot be silently substituted at runtime. The behavior you see in the source is the behavior the simulator runs.
A practical scaling problem with modern server-class SoCs is that a single workstation cannot run the entire regression load — regressions are spread across many cloud instances. UVM is awkward to distribute, because a 64-bit object pointer is meaningless across a TCP/IP boundary; the receiver has no access to the sender’s heap.
A flat-struct payload, by contrast, has a self-contained byte layout. An actor on server A can serialize its outgoing message into the bytes of the struct, ship them across a TCP/IP socket to server B, and the receiving actor can deserialize the bytes back into the same struct on the other side. Crossing the machine boundary becomes a transport concern rather than an architectural one.
A flat-struct architecture crosses these boundaries cleanly. Bridging a UVM object to a C++ model requires manual pack()/unpack() code on both sides; a flat SystemVerilog struct mirrors a standard C struct directly, so DPI-C can carry arrays of payloads to Python or C++ reference models without serialization glue. Hardware emulators (like Cadence Palladium) cannot synthesize dynamic heap allocations, but they handle bit-vector structs natively, which makes static-topology actor frameworks compatible with emulation flows.
The framework has two forms with different runtime substrates. The class-based form (mailboxes, dynamic subscription, run-time allocation) runs on the host-side simulator alongside the RTL DUT — it never crosses to the emulator. What crosses to the emulator is the synthesizable mechanized rewrite of actors (Appendix E), where every dynamic SystemVerilog construct has a static counterpart: bounded parameterized FIFOs replace mailboxes, elaboration-time wired fan-out replaces dynamic subscription, fixed-cardinality interfaces replace dynamic routing tables. The framework’s class form and synthesizable form share the same actor logic and the same typed messages; only the structural primitives at the runtime layer differ. A team that adopts the class form for simulation inherits the synthesizable form for emulation without rewriting the actor code (Appendix E walks the mapping; §7.16 of Chapter 7 walks why this matters operationally for emulator throughput).
Plain structs also map well to waveform viewers (.fsdb, .vcd). Engineers can watch the struct values move through the payload queues cycle-by-cycle in tools like Verdi, instead of reading log lines.
A common hesitation when transitioning to Data-Oriented Design (DOD) is the apparent loss of Constrained Random Verification (CRV) and Functional Coverage, which traditionally bloated inside the data object. Under DOD, we recognize that Constraints and Coverage belong to the Behavior, not the Data.
The data envelope (the struct) crosses the network, while the generation behavior lives in a Generator Actor on the producing side. Constraints move out of the payload and into the Generator, so different Generator Actors can produce edge-case scenarios independently while emitting the same flat struct downstream.
Coverage detaches the same way: a Coverage Actor sits passively on the pub/sub network, sampling immutable deep-copied payload frames as they pass, with no coupling to the active datapath.
No reference counting Restricting message payloads to value-typed structs also removes the lifetime question. When an object handle is shared across multiple subscribers, the runtime needs reference counting (or a garbage collector) to decide when the last subscriber is done so the object can be reclaimed. With deep-copied structs, each subscriber owns its own copy: the copy goes out of scope when the receiving actor’s handler returns, and the simulator reclaims it as part of its normal stack frame.
With value-typed structs as the messages, what remains is the actor itself: a class that owns a mailbox, a handler thread, and a type-indexed map of downstream subscribers it can publish to. Figure 6.3 shows how messages flow between actors using these three pieces.
Because the SystemVerilog mailbox needs a single concrete element type, we cannot drop arbitrary user structs onto a single queue directly. The framework solves this with an auto-boxing wrapper: a small parameterized envelope class that carries any payload struct through a strongly typed mailbox without losing the payload’s type at the receiver.
MsgBase is the abstract base class that travels through every mailbox. It carries the metadata the framework needs to reconstruct causality across an actor topology — a trace identifier, the immediate causal ancestor’s timestamp, the emission time, and the originating actor’s id — plus a stamp() hook that the publishing actor calls to fill those fields in at emission. Concrete envelope types extend MsgBase and declare a payload type via getTypeName().
virtual class MsgBase;
longint unsigned trace_id = 0; // root-cause id, propagated downstream
longint unsigned parent_span = 0; // immediate causal ancestor's timestamp
longint unsigned timestamp_ns = 0; // emission time (set by stamp())
int unsigned sender_id = 0; // originating actor's id
pure virtual function string getTypeName();
// Called by publish() to lock in identity at emission
function void stamp(int unsigned from_actor);
sender_id = from_actor;
timestamp_ns = $time;
if (trace_id == 0) trace_id = _trace_next_id++;
endfunction
endclass
The parameterized class Msg#(type T) extends MsgBase, holds a copy of the struct in its T payload field, reports its payload type via $typename(T), and exposes a static unwrap() helper that safely casts a generic MsgBase back to the typed payload at the receiver. The ‘PUBLISH macro wraps the call site so the user never has to spell out the parameterization or the cast.
class Msg #(type T = int) extends MsgBase;
T payload;
function new(T p);
payload = p;
endfunction
virtual function string getTypeName();
return $typename(T);
endfunction
// Static helper: safely unwrap a generic envelope into a typed payload
static function T unwrap(MsgBase base);
Msg#(T) typed_msg;
if (base == null)
$fatal(1, "Actor unwrap error: null msg passed (expected %s)",
$typename(T));
if (!$cast(typed_msg, base))
$fatal(1, "Actor type-cast error: expected %s, got %s",
$typename(T), base.getTypeName());
return typed_msg.payload;
endfunction
endclass
// Convenience macro: auto-box the payload and call publish() on `this`. `_tmp` is automatic so the macro works from both class methods and module-scope initial/always blocks.
`define PUBLISH(DATA) \
begin \
automatic Msg#(type(DATA)) _tmp = new(DATA); \
publish(_tmp); \
end
A note on ‘PUBLISH’s reason for existing: SystemVerilog cannot infer parameterized class type arguments from a call site — there is no equivalent of C++ template argument deduction or Rust generics inference. So a literal publish would be publish(Msg#(MyTransaction_s)::new(my_tx)), and every publish site would have to repeat the type. The ‘PUBLISH(payload) macro pastes SystemVerilog’s type(…) operator into the call site, so the compiler extracts the payload’s type at elaboration and produces the correctly parameterized Msg#(T)::new(…) call before invoking publish(). The macro is a small ergonomic adapter around a missing language feature, not a load-bearing piece of the framework. Two variants live alongside it: ‘PUBLISH_TO(actor, data) sends directly to a specific actor’s mailbox without going through this actor’s subscriber list, and does not stamp trace metadata — when lineage matters, publish through the wired path (‘PUBLISH or ‘PUBLISH_TRACED, which stamp). ‘PUBLISH_TRACED(data, parent_msg) propagates the parent message’s trace_id so causal lineage survives across the publish.
The Actor base class owns a single mailbox, a single run thread, and a type-indexed map of subscribers — each entry pairs a message type name with the queue of consumers wired to receive that type. The start() method spawns the run thread inside a fork...join_none so the caller does not block; the run thread loops forever, pulling one message at a time from the mailbox and handing it to the user’s act() override.
A note on the design choice: publish() looks up the message’s type name in the subscriber map and fans out only to consumers wired for that type, using try_put so a backed-up consumer drops the message rather than stalling the producer. There is no wildcard or subscribe-to-everything path in the base framework — a tracer that wants to observe several message types from one producer issues one ‘WIRE per type, so the topology is fully visible in the wiring code with no hidden edges.
virtual class Actor;
mailbox #(MsgBase) mbox;
Actor subscribers_by_type [string][$];
string name;
int unsigned id;
int mbox_capacity = 0; // 0 = unbounded
bit is_alive = 1;
process p_run;
int unsigned run_gen = 0; // start/stop generation; stale
// run-forks self-cancel against it
function new(string name = "Actor", int capacity = 0);
this.name = name;
this.id = _actor_next_id++;
this.mbox_capacity = capacity;
if (capacity > 0) this.mbox = new(capacity);
else this.mbox = new();
endfunction
// Typed subscriber registration. Invoked by `WIRE.
virtual function void add_subscriber(string type_name, Actor sub);
subscribers_by_type[type_name].push_back(sub);
endfunction
// Typed dispatch: route only to consumers wired for this type.
virtual function void publish(MsgBase msg);
string tn;
Actor q[$];
msg.stamp(this.id);
tn = msg.getTypeName();
if (!subscribers_by_type.exists(tn)) return;
q = subscribers_by_type[tn];
foreach (q[i])
if (q[i].is_alive)
void'(q[i].mbox.try_put(msg));
endfunction
// Returns 1 only when ALL wired consumers accepted -- explicit
// backpressure signal for callers that need to know.
virtual function bit try_publish(MsgBase msg);
bit all_ok = 1;
string tn;
Actor q[$];
msg.stamp(this.id);
tn = msg.getTypeName();
if (!subscribers_by_type.exists(tn)) return all_ok;
q = subscribers_by_type[tn];
foreach (q[i])
if (q[i].is_alive)
all_ok = all_ok & (q[i].mbox.try_put(msg) != 0);
return all_ok;
endfunction
// USER API: handle one inbound message. Override in subclass.
virtual task act(MsgBase msg);
endtask
// USER API: cleanup hook called by stop().
virtual function void on_terminate();
endfunction
// Default run loop: get one message, hand it to act(). A
// subclass can override run() entirely if it needs a custom
// thread shape.
virtual task run();
MsgBase msg;
forever begin
mbox.get(msg);
act(msg);
end
endtask
virtual function void start();
is_alive = 1;
run_gen++;
// The forked block does not execute until the caller
// suspends (LRM 9.3.2), so a stop() in the same time slice
// cannot kill it via p_run. The generation token closes
// that race: my_gen is captured at fork time, and a stale
// fork (out-lived by a newer start/stop) self-cancels
// instead of becoming a second, unkillable run loop.
fork
automatic int unsigned my_gen = run_gen;
begin
if (my_gen == run_gen) begin
p_run = process::self();
run();
end
end
join_none
endfunction
virtual function void stop();
is_alive = 0;
run_gen++; // cancel any not-yet-started fork
on_terminate(); // cleanup before kill: a self-stop
// from inside act() must still run it
if (p_run != null) p_run.kill();
endfunction
endclass
The ‘WIRE macro wires the topology: ‘WIRE(a, T, b) appends b to a’s subscribers_by_type[$typename(T)] queue, so every subsequent a.publish(…) of a message whose getTypeName() matches $typename(T) fans out to b. Messages of other types from a go to their own subscribers and never reach b. There is no central scheduler — the producer’s per-type subscriber map is the routing table, and the type is the key.
The producer never references its consumers; the consumer never references its producers. The wiring statement lives in the parent that owns both ends — a test top, an env, a supervisor — exactly like a hardware module instantiation that names two endpoints and the wire between them. This is the property that lets the same actor topology run as RTL, as host C++, or as a distributed mesh: the actor bodies are topology-agnostic, and only the wiring code names the connections. There is exactly one wiring primitive — ‘WIRE — and no wildcard “subscribe to everything” shortcut; a tracer or recorder that wants multiple types from one producer issues one ‘WIRE per type, which keeps the topology fully visible at every call site.
Queue-before-start rule. A subtlety worth stating explicitly: ‘WIRE only records the subscriber handle in the producer’s type-indexed map; it does not require the subscriber to have been started. If actor A publishes before actor B has called start(), the message lands in B’s mailbox and B drains it once started. This is the same mailbox-as-queue semantics SystemVerilog gives us; the framework only needs to keep the receive thread idle until start() fires. The reason to call this out is the silent-first-message-lost bug: testbenches that publish immediately at run_phase entry but only call start() on subscribers after some setup will drop messages if the subscribers tear down their mailboxes during setup. As long as the mailbox is allocated by Actor::new() (which it is, by default), publish-before-start is safe.
Failure handling. A second subtlety, also relevant when reading the substrate above. Failure handling is message-based: an actor (or a watchdog observing it) that detects an unrecoverable condition publishes a ChildFailureMsg_s instead of handling it locally, and its Supervisor in actor_supervision_pkg.sv (§6.8) applies a restart strategy (ONE_FOR_ONE, ONE_FOR_ALL, REST_FOR_ONE) that the testbench architect chose. The framework’s stance is the Erlang “let it crash” philosophy: actors do not need defensive error handling in every method, because recovery lives in the supervisor. If you find yourself writing defensive code inside act(), that is a sign you have not configured the supervisor correctly — not a sign the actor model is missing something.
When to compose a router. Because ‘WIRE is type-keyed, the simple case — “every consumer wants one type” — is handled by the base class with no extra machinery. When a typed edge needs richer fan-out semantics — consistent-hash sharding to a worker pool, scatter-gather, least-busy load balancing — the test composes a router actor from actor_routing_pkg between the producer and the workers, and wires both sides with ‘WIRE. The base class stays minimal; richer routing is opt-in.
With the base Actor in place, the traditional verification components — generators, drivers, monitors, scoreboards — can be rebuilt as actors. Communication is asynchronous, so there is no per-component decision about who pulls or pushes: an actor publishes its output, and every consumer wired with ‘WIRE for that output’s message type receives it. Consumers naming a different type from the same producer simply do not appear on that producer’s per-type subscriber list and never see those messages; receiver-side filtering is unnecessary because the producer already routes by type.
The graph is wired by message type, so the message types are the contract between actors. We declare them as plain SystemVerilog structs — one for the top-level routing command, one each for the read and write payloads, and one for each side’s check result:
// Top-Level Routing
typedef struct { bit is_write; } cmd_msg_s;
// Branch Execution Payloads
typedef struct { logic [31:0] addr, data; } write_msg_s;
typedef struct { logic [31:0] addr; } read_msg_s;
// Verification Analytics
typedef struct { bit pass; string err; } wr_check_s;
typedef struct { bit pass; string err; } rd_check_s;
With the message types defined, each leaf actor implements its behavior in task act(MsgBase msg).
For example, the Wr Addr node in the topology below unwraps a write_msg_s payload and drives the address onto its virtual interface. There is no inheritance chain to traverse, and no sequence/driver handshake to set up:
class WrAddrActor extends Actor;
virtual bus_if vif;
function new(string name="WrAddrActor", virtual bus_if v);
super.new(name);
this.vif = v;
endfunction
virtual task act(MsgBase msg);
if (msg.getTypeName() == $typename(write_msg_s)) begin
// Safely unwrap the payload envelope
write_msg_s payload = Msg#(write_msg_s)::unwrap(msg);
// Directly execute physical bus interaction
vif.cb.addr <= payload.addr;
end
endtask
endclass
Similarly, a downstream analytical node like the Scoreboard actor simply sits passively, waiting to react to incoming check events routed from any branch:
class ScoreboardActor extends Actor;
virtual task act(MsgBase msg);
if (msg.getTypeName() == $typename(wr_check_s)) begin
wr_check_s chk = Msg#(wr_check_s)::unwrap(msg);
if (!chk.pass) $error("Write Check Failed: %s", chk.err);
end
endtask
endclass
By entirely isolating the localized behaviors within these pure task act() blocks, we successfully separate the Function (the raw execution payload) from the Form (the structural composition of the hierarchy).
In a UVM testbench, the two are entangled: sequences both define the data and orchestrate where it flows, so changing one tends to require changing the other. In the actor framework, ‘WIRE keeps the Form (the topology) declared in one place and outside the actors themselves.
The base ‘WIRE primitive already routes by message type, so simple “type X goes to consumer Y” wiring needs no router actor. A router actor adds richer fan-out semantics on top: the root router inspects the payload fields of each arriving Cmd message and republishes it as either a write_msg_s or a read_msg_s, with the two branches then running independently. The router translates one type into another; the framework still does the typed dispatch on each edge.
Implementing the topology is a matter of instantiating the actors and calling ‘WIRE for each typed edge in the graph. The SystemVerilog mirrors the figure node-for-node, with no sequencer/driver handshake or phasing scaffolding needed:
class VerificationEnv extends Actor;
// 1. Actor handles, typed `Actor' for brevity. Each is constructed as its concrete Actor subclass in build_actors() below --- Actor itself is virtual and cannot be instantiated directly.
Actor rst, root, scoreboard;
Actor wr_router, wr_checks, wr_addr, wr_data;
Actor rd_router, rd_checks, rd_addr, rd_data;
function new(string name="VerificationEnv");
super.new(name);
build_actors(); // rst = RstActor::new("Rst"); root = RootRouter::new(...); ...
build_topology();
endfunction
function void build_topology();
// Wire each edge of the topology with `WIRE. Every edge names the producer, the message type that flows over it, and the consumer.
`WIRE(rst, ResetEvent_s, root)
// Root delegates: scoreboard hears the cmd, write/read branches each get their own router. All three consumers tap the same cmd_msg_s type from root.
`WIRE(root, cmd_msg_s, scoreboard)
`WIRE(root, cmd_msg_s, wr_router)
`WIRE(root, cmd_msg_s, rd_router)
// Write branch: wr_router emits write_msg_s; checks emit wr_check_s.
`WIRE(wr_router, write_msg_s, wr_checks)
`WIRE(wr_router, write_msg_s, wr_addr)
`WIRE(wr_router, write_msg_s, wr_data)
`WIRE(wr_checks, wr_check_s, scoreboard)
// Read branch: rd_router emits read_msg_s; checks emit rd_check_s.
`WIRE(rd_router, read_msg_s, rd_checks)
`WIRE(rd_router, read_msg_s, rd_addr)
`WIRE(rd_router, read_msg_s, rd_data)
`WIRE(rd_checks, rd_check_s, scoreboard)
endfunction
endclass
A framework is judged by how cleanly it extends. The previous sections developed the actor substrate — value-typed messages, mailboxes, ‘WIRE-based declarative typed topology. The next two sections walk through the parallel actor_pkg extensions that turn that substrate into a working framework. This section covers the core methodology extensions — the packages that shape how the framework’s runtime behaves regardless of what the user is building with it. The section after covers the production deployment extensions — the packages that deploy the methodology at scale into specific verification environments.
Because every actor interacts only through messages, adding an extension does not change the rest of the topology. An extension is just another actor that subscribes to the streams it cares about and publishes the streams it produces. The extensions below supply three runtime pillars — routing of messages within the topology, supervised restart of failed actors, and structured observability of the message stream itself — plus the pattern idioms and lifecycle scaffolding that support them.
A router is not architecturally distinct from any other actor: it is one whose act() method inspects each incoming message and republishes it under a different type or with hash-based selection. The base Actor::publish() already does type-keyed dispatch, so a router exists to add semantics the base class deliberately does not have: consistent-hash sharding across a worker pool, scatter-gather, round-robin load balancing, least-busy selection, or any FSM-driven decision about which subset of downstream consumers should receive a particular payload.
State-driven routing is common enough to deserve a concrete shape. Because a router is just an actor, a state machine is an Actor subclass whose act() dispatches on an internal state and publishes a state-change message when the state advances. The BecomeActor base in actor_patterns_pkg formalizes the push/pop handler-stack version of this idiom; written out by hand against the bare substrate, the pattern is simply:
class FsmActor extends Actor;
typedef enum {INIT, ACTIVE, SLEEP} state_e;
state_e state = INIT;
function void transition(state_e next_state);
$display("[%0t] FSM Transition: %s -> %s", $time, state.name(), next_state.name());
state = next_state;
`PUBLISH(StateChange_s'{new_state: next_state});
endfunction
virtual task act(MsgBase msg);
case (msg.getTypeName())
$typename(Wakeup_s): if (state == INIT || state == SLEEP) transition(ACTIVE);
$typename(PowerDown_s): if (state == ACTIVE) transition(SLEEP);
default: if (state == ACTIVE) begin /* Process standard traffic */ end
endcase
endtask
endclass
Mid-simulation reset and fault recovery are awkward in UVM: a reset has to flush queues, jump phases, and release any locks, and a single missed handle leaves the testbench deadlocked.
The actor framework borrows the Erlang “let it crash” approach. A Supervisor actor watches a stream of ResetEvent_s messages from the virtual interface. When reset asserts, it calls stop() on its children, which sets each child’s is_alive to 0, kills its run process, and invokes its on_terminate() cleanup hook. When reset de-asserts, the supervisor calls start() on each child, and the topology resumes with cleared state.
class ResetSupervisor extends Actor;
DriverActor driver;
MonitorActor monitor;
function new(DriverActor d, MonitorActor m);
driver = d;
monitor = m;
endfunction
virtual task act(MsgBase msg);
if (msg.getTypeName() == $typename(ResetEvent_s)) begin
ResetEvent_s rst = Msg#(ResetEvent_s)::unwrap(msg);
if (rst.asserted) begin
driver.stop(); monitor.stop();
end else begin
driver.start(); monitor.start();
end
end
endtask
endclass
A supervisor is roughly 20 lines and lives in its own actor, so reset and recovery do not get tangled with the testbench’s primary execution path. The corresponding UVM scaffolding (objection-aware phase jumping, manual queue flushes, sequencer drain) typically runs hundreds of lines spread across multiple components.
Because every message flows through a typed mailbox, observability fits naturally as additional typed subscribers. A tracer, latency monitor, or structured logger is wired with one ‘WIRE per message type it wants to observe; reading the wiring code at the env level shows exactly what each observer sees, with no hidden edges and no per-emission cost paid by producers that no observer is wired to. The package ships four such observers — MailboxMetricsActor, TracerActor, LatencyHistogramActor, and StructuredLogActor; the PerfActor sketched below is a minimal stand-in for the latency-histogram idea.
The Perf Actor (latency tracking) A performance-monitor actor records the timestamp of each request as it arrives and looks up the matching response. The latency for that transaction is just $time - start_times[id]:
class PerfActor extends Actor;
time start_times[int]; // Keyed by transaction ID
virtual task act(MsgBase msg);
case (msg.getTypeName())
$typename(Req_s): begin
Req_s req = Msg#(Req_s)::unwrap(msg);
start_times[req.id] = $time;
end
$typename(Resp_s): begin
Resp_s resp = Msg#(Resp_s)::unwrap(msg);
if (start_times.exists(resp.id)) begin
$display("Latency for ID %0d: %0t", resp.id, $time - start_times[resp.id]);
start_times.delete(resp.id);
end
end
endcase
endtask
endclass
Causal Observability and Distributed Tracing A single struct extension transforms every message stream into a distributed trace. Adding trace_id and parent_span fields to the abstract MsgBase, and propagating them on every outbound message, reproduces the OpenTelemetry pattern at the testbench level. A scoreboard mismatch is no longer a leaf observation but a causal chain walking back through DMA \(\rightarrow \) bridge \(\rightarrow \) bus \(\rightarrow \) master to the originating constraint sample.
A telemetry actor subscribed to the same streams can serialize each envelope to JSON Lines or a similar structured format. The resulting log file is a record of the full message stream and is in a shape that downstream tools (analyzers, ML-based fuzzers) can consume directly.
Erlang/OTP and Akka have evolved a small set of recurring idioms that map cleanly onto actor systems but sit beyond the bare send/receive/spawn core of the actor model. actor_patterns_pkg collects them as mixin-style base classes; an actor that wants one of these patterns inherits from the corresponding base instead of from Actor directly.
• AskActor — request/reply with a per-call private reply mailbox (Future-style). The caller creates a dedicated reply channel, attaches it to the request envelope, and blocks on the channel; the responder reads the channel out of the envelope and writes its reply back. The pattern keeps single-use replies from polluting the caller’s main mailbox, which would otherwise mix with the caller’s normal traffic and break the type-keyed dispatch invariant.
• StashActor — defer messages while the actor is in a “not ready” mode, then replay them when ready. Most useful during initialization: an actor that needs to load a configuration file before it can answer requests stashes incoming requests, drains the stash when the config arrives, and returns to normal handling.
• BecomeActor — push/pop a stack of message handlers, switching the actor’s behavior dynamically. State-driven actors (a connection that is “negotiating” then “authenticated” then “established”) use this to keep per-state logic readable rather than dispatching everything from one mega-act() body with a state-discriminated case.
• SelectiveReceiveActor — pattern-match on the mailbox without dequeuing other types. Useful when one message class must be handled in priority over others, or when an actor wants to wait for a specific reply while other messages queue up behind it. Equivalent to Erlang’s selective receive.
The four patterns are independent; SystemVerilog does not support multiple inheritance, so they compose by delegation rather than by mixing in multiple bases. The framework’s convention is to provide each pattern as a standalone base; an actor that needs two patterns simultaneously embeds one as a member of the other.
The supervision package above covers fault tolerance — what happens after an actor crashes. actor_lifecycle_pkg covers the operational scaffolding around an actor’s normal lifetime: how to find an actor by name, how to schedule a future message, how to capture undeliverable messages, and how to enforce start-up order across a topology.
• ActorRegistry — process registry from canonical name to actor handle, modeled on Erlang’s register/2. Static class; any actor in the topology can call ActorRegistry::by_name["scoreboard"] to resolve a peer without explicit dependency injection. Particularly useful when the framework’s ‘WIRE topology is being built dynamically from a configuration file rather than from compile-time elaboration.
• TimerActor — send_after(target, msg, delay) and send_periodic(target, msg, interval). Decouples the “schedule a message” concern from the actor that originated the timer; the timer actor itself is observable like any other and participates in supervision and tracing uniformly. A useful side benefit: a timer that crashes is restarted by the supervisor, so periodic dispatch survives transient faults.
• DeadLetterActor — a diagnostic sink for undeliverable envelopes: when an actor detects a message it cannot route (no subscriber wired for the type, or a full destination mailbox) it calls record(msg, reason). Where actors are written to record their undeliverable envelopes, mis-wired topologies surface here as a stream of dead letters rather than as silent drops, and the test catches the routing bug.
• StartupSequence — orders the start-up of actors so that subscribers exist before their producers publish (e.g., scoreboard before monitor, monitor before driver). The caller groups actors into ordered phases via add_phase(), and the sequence brings each phase up in turn. Avoids a category of race condition that simulation testbenches recurrently hit at run_phase boundaries.
All four are themselves actors, so they participate in supervision and observability uniformly with everything else — a registry that crashes is restarted, a timer that emits is traced, a dead-letter actor’s queue depth is a normal coverage signal.
The previous section’s core extensions (routing, supervision, observability, and their pattern and lifecycle supports) are present in essentially every actor program; the framework’s runtime would feel incomplete without them. The five extensions in this section are different in character: they are the packages a verification program reaches for when it is ready to deploy the methodology against a real verification problem at production scale. Each addresses one operational concern — the verification-side stimulus, the persistence and reproducibility of regressions, the register-access abstraction, the cross-process / cross-machine fan-out of the framework’s typed-message bus, and the per-actor unit-testing kit. A team can use the framework without these packages and still get value; the packages here are what turn a working framework into a fielded methodology.
UVM tends to embed covergroups inside transaction objects or monitor classes, which couples coverage sampling to the lifetime of those components. In the actor framework, coverage is just another typed subscriber: a CoverageActor is wired with ‘WIRE to the producer for each message type it samples, and runs its covergroup.sample() call in its own act() method. Adding or removing it does not affect the rest of the testbench.
The package builds this decoupled-subscriber pattern out into a small set of verification primitives, all subclassing Actor:
• ConstraintActor — the stimulus generator: it produces constrained-random transactions and publishes them onto the typed bus for the DUT-driving actors to consume. The constraint solver behind it has a synthesizable form too: Appendix F compiles a production constraint set — an open-source RISC-V instruction generator — to fabric, so the ConstraintActor can ride the emulator with the rest of the graph rather than staying on the host.
• CoverageActor — wired via ‘WIRE for the payload types it samples; runs covergroup.sample() in its act() handler.
• ScoreboardActor — subscribes to the DUT’s observed outputs and checks them against a reference model: the pass/fail oracle as one typed subscriber, not a web of TLM ports.
The same one-‘WIRE pattern is how a team adds its own domain-specific subscribers without touching the rest of the testbench — a power monitor that integrates bus-switching activity into Joules, a security monitor that flags illegal trans-domain reads against a TrustZone-style isolation invariant, a functional-safety monitor that tracks ASIL-D fault-detection coverage per ISO 26262. Each is a small Actor wired in one line: ‘WIRE(monitor, MsgT, new_actor), receiving only the typed messages it asked for and nothing else. In UVM, each such axis would constitute a new agent with its own factory registration and connect_phase() wiring.
Specification as a Runnable Actor Functional safety regimes are prone to drift between English specifications and RTL implementation. The actor framework addresses this directly: compile a TLA+, Alloy, or Cryptol specification to a SpecActor that consumes the same input message stream as the DUT and emits its own expected output stream. A DiffActor subscribes to both output streams and flags divergence in real time. The specification is no longer a static document that drifts — it is a runnable verification component held to the same continuous-integration regime as the implementation. This pattern is the actor-framework realization of a tradition with industrial pedigree: Centaur’s ACL2-based executable specifications for x86, the seL4 Haskell-style executable spec that drives the proof-side equivalence check, and the Lean #eval-driven approach where the specification doubles as a sanity-checkable simulator (Chapter 3 §3.6). The actor framework integrates it as a runtime substrate rather than as a separate proof artifact.
A four-hour overnight regression that fails at hour three is operationally useless if the failure cannot be reproduced. The actor substrate makes a tap-and-replay reproducer practical: because every message is a packed struct flowing through a known topic, the RecorderActor (in actor_persistence_pkg.sv) subscribes to the bus, timestamps each envelope, and serializes the full stream to disk.
A companion ReplayActor reads that log and republishes the messages with original timing, bypassing the upstream stimulus generators. This collapses days of debug into a 30-second reproducer — but only when three conditions hold (also documented in the actor_persistence_pkg.sv header):
1. The DUT is reset to the same starting state.
2. All non-replayed actors are deterministic — no $urandom calls outside the replayed stream.
3. The transport preserves message ordering. (True for in-process mailbox; for the distributed bridges in actor_distributed_pkg.sv, this requires a strict-order transport like NATS JetStream.)
When all three hold, replay is bit-deterministic. When any one fails, replay produces a different trace, and the reproducer is not actually reproducing the original failure. Stating these conditions explicitly is the difference between record/replay being a debugging tool and being a hopeful gesture.
A related research direction — not yet shipped in actor_pkg/ — is actor-to-formal export: walking each actor’s act() body with an AST translator and emitting a TLA+ or SMT-LIB encoding for a formal prover to check for routing deadlocks or dropped messages. Because actor interactions are confined to typed mailbox sends and receives, the message-passing surface is small enough to be a plausible target for such a translator, but the translator itself is future work.
UVM’s RAL maintains four copies of every register-field value (Desired, Mirrored, Reset, Value) plus a predictor that updates the mirrored copy from observed bus traffic. For CSR-heavy SoCs that is gigabytes of testbench state shadowing state that already lives in the RTL. Chapter 5 §5.14 discusses the cost; the actor framework can do better.
actor_ral_pkg::RalActor keeps a different bargain. Definitions are immutable contract data: name, address, bit slice, access policy, reset value. Current values are not shadowed — they live in the RTL (or in the IP-actor’s slave-side backing store) and are read via backdoor when a test needs them. The bus monitor’s stream of bus packets gets translated to symbolic RalEvent_s for downstream coverage / trace / scoreboard subscribers. Memories follow the same rule with even less state: a memory is just a (base_addr, size, backdoor_root) triple.
The API surface is small: define_reg / define_field / define_mem populate the contract; addr_of(name) maps symbolic name to physical address; name_at(addr) is the reverse for trace and coverage; read_field / write_field / read_mem / write_mem are backdoor pass-throughs that resolve the symbolic name and call into a RalBackdoor handle attached at construction. Tests that need backdoor access attach a backdoor; tests that don’t can subscribe to RalEvent_s and observe register accesses by name without ever forcing a value.
Per-IP register definitions are auto-generated from each block’s specification. appC_earlgrey/tools/reggen_actor.py reads OpenTitan-style Hjson register descriptions (the same Hjson format OpenTitan’s reggen consumes
when it generates the UVM RAL) and emits one SV file per IP containing a define_
Chiplet-based SoCs do not share an execution space across dies; every cross-die interaction — even coherent memory sharing — travels as explicit die-to-die (D2D) protocol messages. Modeling them in a single monolithic testbench can be awkward because the testbench introduces coupling that the silicon does not have. An actor-based testbench does not impose that coupling: each chiplet’s verification components are already separate actors that exchange messages.
SystemVerilog mailboxes are in-process, so cross-machine communication needs a transport. A transport bridge — a TransportBridgeActor subclass from actor_distributed_pkg — is wired via ‘WIRE for each message type it forwards; the base class serializes each envelope and the subclass pushes the bytes to a network transport (ZeroMQ, NATS JetStream, Iceoryx, or libfabric). A corresponding bridge on the other side feeds messages back in. Above the bridge, the rest of the testbench keeps working with the same ‘WIRE edges:
// User subclass of TransportBridgeActor: supply the transport binding by overriding send_bytes()/recv_bytes(). The base handles `WIRE subscription, serialization, and batching.
class ZmqBridgeActor extends TransportBridgeActor;
function new(string name = "ZmqBridge", string ep = "tcp://*:5555",
string top = "actor.bus");
super.new(name, TRANSPORT_ZMQ, ep, top);
zmq_dpi_init_pub(ep);
zmq_dpi_init_sub(ep, top);
endfunction
virtual function void send_bytes(byte unsigned bytes[]);
zmq_dpi_pub_send(topic, bytes);
endfunction
virtual function int recv_bytes(output byte unsigned bytes[]);
return zmq_dpi_sub_recv(bytes);
endfunction
endclass
A Coverage Actor running on a separate machine subscribes to the same logical topic and aggregates coverage from a thousand parallel regression workers.
Forward- and backward-compatible message evolution across asynchronously-shipping chiplet teams is a natural extension the flat-struct discipline invites, rather than a feature the framework ships today: because every payload is a plain struct, a protobuf-style Interface Definition Language can generate per-language bindings (SystemVerilog, C++, Python, Rust) from one canonical source, and a version field on each message lets older subscribers take a forward-compatible projection and ignore unknown trailing fields. That contract mechanism is what would let chiplet teams ship independently without compile-time coupling.
The actor model’s strict mailbox-only interaction means each actor is genuinely unit-testable without standing up the full topology. actor_test_pkg ships three primitives for unit-testing actors in isolation; together they let a per-actor unit test run in milliseconds with no DUT, no clock, and no simulator-side scaffolding beyond the actor under test.
• ProbeActor — passive sink that captures every message of the wired type into a queue for later assertion. One ‘WIRE edge per message type the probe should observe; no additional configuration.
• FakeActor — programmable response. The test wires a rule set that maps incoming message patterns to outbound replies, simulating an upstream producer or downstream consumer without standing up the real one. Useful when the actor under test has many wired peers and the test wants to vary the response shape of one specific peer.
• ExpectKit — assertion helpers: expect_message(probe, type, timeout), expect_no_message(probe, type, within), expect_count(probe, type, n). The kit reduces the per-test scaffolding to a handful of one-line assertions, so the test reads as a behavioral contract rather than a pile of mailbox-polling boilerplate.
A typical actor unit test stands up a ProbeActor, wires it to the actor under test, drives one or two stimulus messages, and asserts via the expect-kit. The pattern scales: every actor in the framework can be tested this way; the resulting per-actor unit-test suite runs in seconds — the per-component CI cadence that UVM’s tightly coupled component model makes hard (a component rarely runs without its surrounding agent, sequencer, and config wiring), and that the actor’s mailbox-only interface makes routine through actor_test_pkg. The test kit is small (the ProbeActor is roughly thirty lines; the ExpectKit is roughly forty); the methodological payoff is the regression cadence it makes routine.
The chapter so far has focused on what the actor model gives verification: a topology that matches the silicon, fewer lines of code, cleaner cross-IP causality, distributed regression for free. The framing has been “actors replace UVM.” That framing is true but incomplete. The deeper claim is that hardware design has been carrying four parallel codebases for one design — a specification document, an architecture model in C++/SystemC, an RTL implementation in SystemVerilog, and a UVM verification environment also in SystemVerilog — because the languages and frameworks at each phase pick mismatched models of computation. The actor model collapses those four into one, because each phase is a refinement of the same shape rather than a translation into a new one.
Why the model carries through. Section 6.3 (“Hardware Circuit Elements are Actors”) established that silicon is structurally an actor system: independent blocks with local state, communicating through fixed wire topology, no shared memory, no sequential main thread. Chapter 1’s Models-of-Computation discussion separated computational power (what problems a tier solves) from structural style (how computation is organized). Hardware’s structural model of computation is concurrent message passing on wires — and that is exactly what the actor framework expresses in software. The spec, the architecture model, the verification environment, the synthesized RTL, and the FPGA emulation all share one structural shape because that shape is the shape of the silicon they describe.
The seven-step continuum. The implications for the design flow follow directly:
1. Specification written in actor code: typed messages are the interface contract; each block’s externally observable behavior is the actor’s act() method. The spec is executable.
2. Architecture exploration runs the actor model. Performance, latency, power, throughput, fault-recovery times — all measurable from the actor system because each actor reports its own activity. Change one actor, rerun in seconds. Validate the spec works before any RTL is written. The model/ side of the OpenTitan example in Appendix C is exactly this: twenty-eight behavioral IP actors composing a chip-scale Earl Grey at architectural level, with the full verification framework already attached.
3. Verification is the same TB topology already written in step 1. Scoreboards, coverage actors, RAL, supervisors, observability subscribers — no separate UVM environment to build. Every claim made in the spec becomes a property the same TB can check.
4. Synthesis is the actor framework’s restricted synthesizable form (Appendix E). Each actor’s act() state machine maps to a synthesizable RTL block; typed messages map to wire interfaces; mailboxes map to FIFOs; ‘WIRE edges map to elaboration-time port connections. Bluespec, Chisel, and Amaranth show high-level concurrent languages can synthesize; Kami (a Coq-to-Verilog framework, MIT) shows the stronger correct-by-construction precedent — it has produced RISC-V cores running on FPGA, extracted from machine-checked Coq proofs of the architectural specification, with the proof-plus-extraction pipeline carrying the correctness guarantee. An actor-DSL that synthesizes is a natural next step in the same lineage.
5. FPGA emulation. Synthesized actor RTL becomes a bitstream and runs on FPGA at MHz speeds. The same actor framework runs in software simulation, on commercial RTL simulators, and on FPGA emulators — only the substrate underneath changes; the actor topology and the verification scoreboards are identical across all three.
6. AI-driven design. Today’s AI tools generating RTL from English specifications hit a large specification gap: English has to be parsed into structured concurrency. When the input is actor code, the gap collapses to a shape-preserving translation: one structured concurrent representation into another. AI as a participant becomes tractable at every phase, not just final RTL generation. One structural caveat worth naming honestly: AI-assisted theorem proving (Chapter 3) has a kernel oracle that mechanically rejects malformed proofs, which is the property that lets LLMs participate without compromising soundness. AI-RTL generation has no equivalent kernel; the equivalence check against the actor specification (Step 7 below) is the closest substitute and is the structural safeguard the methodology relies on.
7. Verification by construction. If RTL is derived by verified synthesis from the actor specification, the derivation carries the equivalence guarantee; if it is AI-generated, the explicit equivalence check of step 6 supplies it. Either way, the verification effort spent in step 3 verifies the spec, not the implementation.
What the rest of this book demonstrates. Step 2 (architecture exploration) and step 3 (verification) are demonstrated end-to-end in Appendix C: the model/ subdirectory runs all twenty-eight Earl Grey IPs as actors with the full verification framework, and the appC_earlgrey/dv/ip_uart/ testbench drives real OpenTitan UART RTL through the same framework’s symbolic-name RAL — proving the same actor topology works whether the DUT is a behavioral actor or a real RTL block. Steps 4–7 are developed in the appendices: Appendix D unfolds the continuum end-to-end, Appendix E takes the synthesizable form through place-and-route, Appendix G performs the emulation substrate swap, and Appendix H treats AI-driven generation. The point of stating the seven steps explicitly is that the methodology contribution of this book is not just a verification framework — it is a specification format, an architecture-exploration substrate, a verification environment, and a structured input for AI-assisted RTL generation, all in one model. Picking the correct model of computation once carries forward through every subsequent phase.
Framework line counts in this chapter refer to the implementation in actor_pkg/; example and UVM totals are wc -l over the named directories. The framework breaks down into three nested tiers:
• The actor abstraction itself fits in roughly 50 lines of core: Actor base class, MsgBase, Msg#(T), publish(), ‘WIRE, mailbox plumbing. This 50-line core is the substrate everything else in the chapter extends. The full actor_pkg.sv file is 233 lines: the class substrate (MsgBase, Msg#(T), Actor, with the trace IDs and supervision flags) runs to about line 185, and the remaining lines provide the ‘WIRE/‘PUBLISH/‘PUBLISH_TRACED macros that any production deployment wants.
• A useful verification setup — one stimulus actor, one scoreboard, one coverage actor, one recorder, one supervisor — lands in roughly 500 lines, because each verification component is itself only 50–100 lines. The eight feature-demo testbenches under ch6_actor_examples/01_hello_actor/ through 08_testkit_demo/ each isolate a single component and run from roughly 70 to 210 lines apiece; the ~500-line figure is reached only when stimulus, scoreboard, coverage, recorder, and supervisor are composed into one environment. Larger integration cases live alongside them: 09_integration/ composes a multi-feature SoC-style demo, while the UVM-to-actors UBUS rewrite (appA_actor_ubus/, Appendix A) and the twenty-eight-IP Earl Grey demonstration (appC_earlgrey/, Appendix C) ship as appendix examples.
• The full production framework — routing, supervision, observability, verification, persistence, distributed transports, RAL, lifecycle, patterns, test-kit — is approximately 2,100 lines spread across the core actor_pkg plus ten sibling packages: the entire SystemVerilog portion of the actor_pkg/ directory. The C++/SystemC port at actor_pkg_systemc/, the synthesizable form at appE_synth/, and the pure-C++ port at actor_pkg_cpp/ add further code that the appendices develop.
For comparison, the UVM Base Class Library is on the order of 50,000 lines of source code to establish its phase synchronization and TLM routing mechanics. A standard UVM Agent requires 500–1,000 lines of inherited boilerplate; the equivalent in the actor framework is 50–100 lines. The size delta on the UBUS DUT specifically (Chapter 5’s case study) is roughly 4\(\times \): the legacy UVM environment is about 2,900 lines, and the actor rewrite under appA_actor_ubus/ is roughly 740 lines for equivalent coverage of the same DUT.
The architectural reason matters more than the size delta. Because UVM is a tightly coupled, inheritance-rich OOP framework, adding a single field to a transaction object often forces cascading rewrites across the sequence, driver, monitor, and scoreboard. In the actor framework, the equivalent change is one field added to a struct; subscribers that do not consume the new field do not need to change. Across a serialized transport boundary, unaffected subscribers do not even need recompilation; in-process, they simply ignore fields they do not read.
A small, well-structured architecture tends to outlast a heavy institutional standard as the work scales up. Multi-SoC verification needs a methodology that scales horizontally; the actor model does so by construction, whereas UVM, in its current form, does not.
This framework is not a proposal to rip and replace legacy UVM codebases overnight. The industry shift toward data-oriented verification will take years, and the actor framework can coexist with UVM at integration boundaries. The complexity wall is real, however, and Chapter 5’s UBUS environment makes it concrete.
The empirical close: UBUS as actors. The same UBUS DUT walked through in Chapter 5 lives in this repository as an actor-based testbench at appA_actor_ubus/. Reading those files alongside the legacy UVM implementation makes the framework’s argument empirical rather than rhetorical:
• ubus_master_actor.sv replaces the master agent + sequencer + driver.
• ubus_slave_actor.sv replaces the slave agent + responder.
• ubus_protocol_monitor_actor.sv replaces the bus monitor.
• ubus_scoreboard_actor.sv is wired (‘WIRE) to the monitor’s UbusMonPkt_s stream and pairs request/response transactions.
• ubus_coverage_actor.sv subscribes to the same stream as a parallel observer.
• ubus_stimulus_actor.sv replaces the test sequence library; it publishes randomized transactions and signals completion after a fixed iteration count.
• ubus_env_actor.sv composes the topology with ‘WIRE calls — the actor equivalent of uvm_env::connect_phase.
Total testbench code is on the order of 740 lines, against \({\sim }2{,}900\) for the legacy UVM environment. The reduction comes not from clever encoding but from the absence of agent boilerplate, the absence of factory wiring, and the absence of phasing scaffolding — replaced by direct ‘WIRE typed edges and one shared message envelope discipline. The reader is invited to run make -C appA_actor_ubus and read the full source; the implementation is the argument.
Looking forward. Appendix M (AI Hardware Systems with Actors) extends the methodology up the hardware-systems ladder. It maps the same framework primitives onto GPUs, AI accelerators (TPU, Trainium, Cerebras, Groq, Tenstorrent), distributed training and inference clusters, and robotic platforms — the modern hardware systems where concurrent message-passing is the natural shape of the work. The four standard parallelism strategies of distributed deep learning become four ‘WIRE topology patterns, inference-serving stacks like vLLM reduce to actor topologies with backpressure, and the neuron itself becomes an actor with the autograd graph falling out of the framework’s lineage infrastructure.
The remaining twelve appendices (A through L) develop the substrate at increasing scale and across adjacent languages and tools — the preface catalogues them by topic. Chapter 7, next, picks up the third leg of the EV pipeline.
The actor substrate also has open threads beyond what these appendices develop: cross-die message contracts for chiplet verification, graph-based rule processing on the message stream, and the actor-to-formal export sketched earlier in the persistence subsection. Those remain on the agenda that the book opened in Chapter 1.