Roadmap¶
cpptb already provides the low-level foundation needed to write concurrent testbenches: typed DUT and hierarchy access, arbitrary-clock edge waits, physical delays, simulator-phase waits, typed tasks, process control, events, queues, timeouts, wide and fixed-point values, multidimensional arrays, packed views, bounded queues, locks, semaphores, and force/release. Every completed runtime feature has an exact SystemVerilog workload in the authoring benchmark suite.
The next milestones focus on verification productivity rather than adding more ways to suspend a coroutine. The goal is to make tests easy to organize, reproduce, debug, and reuse while keeping DUT reads, writes, and time advancement explicit.
The API reuses familiar cocotb names where behavior matches. Signal access
remains explicit C++ (get(), set(), deposit(), force(), and
release()), and physical time remains an explicit Delay.
Framework scope¶
This roadmap now separates the reusable framework from the optional reference harness. Framework milestones cover C++ APIs and semantics that remain useful when cpptb is embedded in another build, regression, or CI system:
scheduler, DUT access, test lifecycle, result, and verification-component APIs;
generated typed bindings and simulator transport contracts; and
metadata and callbacks that any external harness can consume.
The public cpptb command and lower-level cpptb-run are a maintained
reference harness. Future CLI filtering policy, process orchestration, JUnit
conversion, waveform reruns, wall-time limits, reproduction-command
presentation, and build diagnostics are harness work. They are not framework
milestone blockers. The current development priority is the framework.
Status summary¶
# |
Milestone |
Status |
Outcome |
|---|---|---|---|
- |
Done |
Typed DUT access, timing, concurrency, hierarchy, data types, and simulator backends |
|
1 |
Done |
Owned test processes, terminal states, checks, and harness-neutral diagnostics |
|
2 |
Done |
Bounded FIFO communication, locks, semaphores, and cancellation-safe handoff |
|
3 |
Done |
Package-ready bus and stream interfaces, APB components, monitors, checkers, and scoreboards |
|
4 |
Done |
Reproducible random streams, adaptive solving, and mergeable functional coverage |
|
5 |
Done |
Optional protocol-independent memory and typed register models with frontdoor and backdoor adapters |
|
6 |
In progress |
Typed interfaces and inout intent are shipped; four-state and second-simulator conformance remain |
|
7 |
In progress |
Process-aware logging and scheduler wait diagnostics are done; waveforms and distributable packages remain |
|
8 |
Planned |
Runtime clock ownership and explicit reusable reset components |
|
9 |
Planned |
Reduce DPI scheduler resumptions across fine-grained timing boundaries |
|
10 |
Done |
Produce the discovery outputs without executing user test code during the build |
|
- |
No priority |
Deferred interoperability, protocol-component, synchronization, lookup, and harness work |
Select a milestone to jump to its detailed scope below.
Foundation¶
Status: Done
The simulator timing model is implemented. ReadWrite{}, ReadOnly{}, and
NextTimeStep{} are backed by the direct Verilator timing backend and the
standard VPI fallback. The scheduler enforces legal phase transitions and
rejects writes during ReadOnly with an actionable diagnostic. The generated
SV-DPI event calendar remains an experimental backend with a deliberately
documented event-discovery limitation.
The repository also has an internal regression orchestrator and a reference command-line harness. Those tools validate and demonstrate the framework, but their future feature set is tracked separately from framework completion.
1. Framework test lifecycle and structured results¶
Status: Done
The framework lifecycle milestone includes:
multiple registered tests with catalog listing and exact per-invocation selection through embeddable C++ APIs;
nonfatal
expect()andexpect_eq()checks plus fatalrequire()andrequire_eq()checks;structured warnings plus test-owned process cleanup, stable process IDs, spawn-site provenance, and exception attribution;
failure-only diagnostic formatting for integers, symbolic enums,
Bits,LogicBits, ranges, and generated packed views, with a custom formatter extension point;skip, expected-failure, unexpected-pass, and simulation-time-timeout outcomes with explicit success rules;
discoverable tags and typed parameterized cases with stable catalog names;
versioned JSON results and optional in-process
ResultSinkcallbacks; andan exact C++/pure-SV
test_lifecycleperformance pair under the standard1.10xhard guard.
The API, semantic coverage, and performance-qualification machinery are implemented. A current-policy formal baseline refresh for the realistic process gates remains pending an admitted low-load host run. The benchmark suite distinguishes persistent verification processes from repeated dynamic process creation. The generic scheduler process pair remains a hard-gated decomposition check. Realistic lifecycle qualification uses the hard-gated persistent-monitor and finite driver/worker/scoreboard workloads; repeated lifecycle-owned zero-time children remain a diagnostic fixed-cost control rather than a framework release gate.
See Framework test lifecycle for the API, terminal-state rules, metadata model, and formatter extension point. Diagnostic strings are constructed only on failed equality checks; the passing-check hot path remains free of diagnostic allocation.
The reference harness already demonstrates catalog discovery, exact selection, fresh-process execution, result files, and cached builds. CLI tag filtering, JUnit conversion, waveform-on-failure reruns, wall-time policy, reproduction commands, and presentation of build or infrastructure failures are parked harness enhancements. Deterministic random streams remain in milestone 4; only their result metadata belongs in the lifecycle model.
2. Bounded queues and synchronization¶
Status: Done
Queue<T> provides optional capacity and producer backpressure as the single
public FIFO communication primitive. The implemented API includes:
optional capacity and producer backpressure;
get_nowait(),put_nowait(),empty(),full(), andsize();cancellation-safe FIFO waiter ordering;
cancellation-safe reserved-item, reserved-slot, lock-handoff, and permit recovery.
Idle synchronization objects do not register callbacks or add
simulator-boundary traffic. Barrier remains deferred until a real
multi-process example demonstrates semantics that are not already covered by
Join, Event, or Semaphore.
3. Reusable verification components¶
Status: Done
The first lightweight transaction-component slice is implemented:
PutPort<T>producer endpoints andGetPort<T>consumer endpoints;connectable queue-backed implementations without requiring components to depend on a concrete
Queue<T>;AnalysisPort<T>for nonblocking fan-out from a passive monitor to multiple subscribers, plus an explicit queue adapter for subscribers that need asynchronous consumption;InOrderScoreboard<T>for immediate transaction pairing and structured nonfatal mismatch reporting; andready/valid driver and monitor helpers with explicit sample edge and delay.
The runnable component FIFO example composes
these pieces without replacing the lower-level fifo_scoreboard example. An
exact analysis_fanout C++/pure-SV benchmark covers every publication and
delivery and remains below the repository’s 1.10x performance guard.
The protocol-neutral transaction slice is also implemented:
a
MemoryMappedMasterconcept for typed reads and writes with addresses, data, byte enables, and explicit responses;StreamSourceandStreamSinkconcepts for packet or word streams;direct coroutine transaction methods that serialize concurrent callers when required without exposing scheduler or arbitration machinery;
reference-model adapters and keyed or out-of-order scoreboards; and
explicit active master/source/sink roles and passive monitor/checker roles without a mandatory agent or environment hierarchy.
APB is the first complete protocol component. Its master, passive monitor,
protocol checker, structured transactions, runnable example, scoreboard, and
coverage composition are implemented. The same sequence is reusable through
the generic memory-mapped interface. The exact apb_component C++/pure-SV
pair exercises the active and passive components under the standard 1.10x
performance guard, certified at 1.0412x in the August 8, 2026 admitted run.
Typed transaction recording is also implemented in cpptb_vc. Timed
TransactionObservation<T> envelopes, monitor-owned analysis output, static
field descriptors, uniquely named recorder streams, an in-memory sink, and a
JSON Lines sink keep protocol decoding separate from persistence. The runnable
APB transaction trace records and checks the same 256
operations in C++ and pure SystemVerilog. The existing apb_component pair is
the disabled-recorder overhead guard. The transaction_recording authoring
pair retains 200,000 equivalent records in C++ and pure SystemVerilog and is
the enabled-recorder 1.10x hard gate, certified at 1.0414x in the
August 8, 2026 admitted run.
The recording design decisions are recorded here rather than in the user
guide. The monitor owns its typed analysis output and the recorder subscribes
to it like any other passive consumer; a record_to(recorder) configuration
call on the monitor was rejected because it would create a second connection
model beside analysis ports and make recording look more privileged than
scoreboarding or coverage. A string-based dynamic field builder was rejected
as the documented API because it is typo-prone and hard to refactor; the typed
explicit stream write remains available as the implementation-level primitive.
Observed transactions are the verification ground truth and stimulus intent is
separate diagnostic context; the API keeps them as separately named streams
and deliberately excludes recorder-side address, data, or timing-window
matching, which would be ambiguous under multiple initiators, retries, or
third-party traffic. The monitor API also changed before 1.0 so that callers
no longer create and pass an AnalysisPort<T>& into run(...).
Deliberately deferred until concrete use cases arrive: driver-side intent
recording (a proposed record_intent_to(...) API in which one intent record
spans driver acceptance through the returned response and reuses the core
logging layer’s process provenance); an explicit typed correlator that
publishes a parent stream-and-sequence reference from intent to observation;
live begin/end event streaming and simulator waveform-database sink adapters;
automatic monitor flush and aborted-record emission during coroutine
cancellation (the envelope’s Aborted and Incomplete dispositions reserve
the shape); migrating the ready/valid monitor from its bare payload to an
observation envelope or a transfer payload carrying stall counts; AXI
burst-beat parent/child visualization; and a dynamic string-to-value
transaction schema. The completed-record envelope, named streams, and
per-stream sequence numbers reserve a path to these features without making
them part of the current public contract.
The implementation lives under the separate cpptb_vc include tree,
namespace, and CMake target. It depends only on public core APIs and generated
signal values supplied by the caller, preserving a clean path to a separately
versioned component package. See
Verification components.
Add AXI-Lite, streaming, UART, or SPI only when each component is backed by complete runnable examples and an exact SystemVerilog performance peer.
These remain ordinary C++ objects and coroutines, not a second scheduler or a mandatory component hierarchy.
The endpoint layer should separate connectivity from storage in the useful
part of the UVM TLM model. A producer calls a put interface, a consumer calls a
get interface, and a monitor publishes through an analysis interface. A
Queue<T> may implement the put/get pair, but components should be testable
with another transport. Analysis publication must remain nonblocking; a
subscriber that needs asynchronous consumption connects through a buffered
adapter rather than silently delaying every other subscriber.
Transaction methods may encapsulate the signal writes and waits that define a protocol operation, but the component implementation must show every signal write, edge wait, sampling phase, and delay. The framework must not insert implicit resets, unrelated waits, clock startup, or signal writes.
4. Reproducible random stimulus and functional coverage¶
Status: Done
The random-generation and constrained-random slices are implemented:
test.random()returns the current process’s deterministic stream;randint(),choice(),weighted_choice(), arbitrary-widthrandbits(), andshuffle()cover the common value-generation path;the master seed and versioned algorithm name are retained in schema-version 4 structured results and accepted by the reference runner;
independently derived process streams make concurrent generation insensitive to coroutine interleaving; and
the exact
random_stimulusC++/pure-SV pair checks every response and final checksum under the standard1.10xguard.user-defined transactions use
Randomized,Rand<T>,RandC<T>, named constraints,randomize(),randomize_with(...),pre_randomize(), andpost_randomize();membership sets and ranges, weighted distributions, soft defaults, and runtime constraint handles cover common SystemVerilog-style policy control;
nested randomized objects, fixed
RandArray<T, N>fields, and arbitrary-widthRandBits<Width>values compose into one flattened solve problem;a backend-neutral typed constraint representation separates authored transaction classes from solving policy;
the default adaptive backend keeps the sampling path dependency-free and invokes an application-configured fallback only after search exhaustion;
the optional direct Z3 backend caches persistent translated models, handles coupled constraints, selects replayable randomized assignments, and reports named unsatisfiable cores;
structured results record the configured backend, backend version, and the number of sampling and solver executions;
the exact
constrained_packetC++/pure-SV pair uses the same random stream, candidate rejection, DUT transactions, checks, and checksum under the standard performance policy; andthe exact
constraint_extensionspair exercises the new constraint and composite-field semantics against an equivalent pure-SystemVerilog workload;Covergroup<T>and typed coverpoints support ordinary, ignore, illegal, transition, and crossed bins with explicit allocation-free sampling; andcoverage snapshots report percentages and stable model data, merge matching seeds, reject mismatched models, and serialize to schema-version 1 JSON.
CRAVE remains a possible future adapter, not a required or API-defining dependency. Its viability spike required compatibility repairs for modern CMake, macOS, Clang, and Z3 that the direct Z3 adapter does not require.
See Randomization for the feature guide and side-by-side examples comparing cpptb, Cocotb, UVM, and pure SystemVerilog authoring styles.
The exact coverage_sampling C++/pure-SV pair performs equivalent point,
ignore, illegal, transition, and cross accounting for each DUT transaction and
passes the standard 1.10x hard guard. See
Functional coverage for API and report
examples. UCIS interoperability remains a follow-on after the schema-1 model
has been exercised by larger regressions.
Random stimulus and coverage must remain separate facilities: users should be able to use deterministic directed stimulus with coverage, or random stimulus without adopting a coverage model. Coverage-hole selection may later generate values from uncovered bins, but it is an optional policy layered on the same random and coverage APIs rather than a prerequisite for either facility.
5. Memory and register verification components¶
Status: Done
The protocol-independent sparse memory model is implemented:
allocated regions, byte enables, configurable endianness, and read/write permissions;
expected-data regions with contextual mismatch diagnostics;
read, write, and error-injection callbacks; and
explicit image load, fill, inspect, and dump helpers;
caller-owned
read_into()storage and allocation-free integer word access.
The first typed register-model slice is also implemented:
registers, fields, memories, access policies, and reset values;
frontdoor operations through the generic
MemoryMappedMasterinterface;backdoor access through generated hierarchy paths;
mirrored values, prediction, volatile fields, and read/write checking; and
generation from SystemRDL and IP-XACT through a standard PeakRDL exporter plugin, plus direct native RgGen YAML/JSON/TOML import;
preserved
accesswidth, generated-file provenance, and path-qualified diagnostics for unsupported widths and reset references;fieldless and write-only prediction, readable-only mirror checking, and warnings for staged states that field write effects cannot reach; and
reset-mask-aware staged and mirrored validity plus passive prediction from monitored bus transactions, including partial byte enables;
generated typed backdoor adapters from standard SystemRDL
hdl_pathandhdl_path_slicemetadata, with compile-time hierarchy pruning and no implicit delay; andgenerated register-backed memory frontdoors and hierarchy backdoors with per-operation path selection, raw
peek/poke, and allocation-free span operations; anda pinned secworks AES ground-truth suite whose generated model reproduces the unchanged upstream bench’s complete 720-event register transcript and all 20 NIST cases.
The memory and register layers should not own simulation time. A frontdoor adapter exposes ordinary coroutine transactions, and a backdoor adapter performs explicit hierarchical operations. This keeps both models reusable across APB, AXI-Lite, Wishbone, and custom buses.
Named generated field and memory handles, relocatable block bases, staged and
mirrored values, every standard SystemRDL write effect, write-once policy,
passive memory prediction, generated-model execution against a fake master,
the APB-backed example, and a first-class IP-XACT register-and-memory example
with a matching pure-SystemVerilog sequence are covered by regression. The
exact memory_model C++/pure-SV semantic pair passes at 100,000 operations
and is certified at 1.0247x under the 1.10x hard guard in the August 8,
2026 admitted run; the bus-free memory_model_direct pair also matches at
200,000 model operations, 300,002 checks, and zero simulated cycles, with its
formal timing still pending an admitted low-load run.
The scalable AES ground-truth workload has a historical diagnostic measurement
of 1.587x pure SV for 3,600 cases. That run exceeded the current host-load
admission limit, and the ratio also exceeds the performance guard; semantic
equivalence is exact. The
architectural experiments intended to address this shared runtime cost are
tracked in milestone 9.
The RAL milestone was completed in the following order so correctness and observation semantics landed before convenience and broader interchange support:
Order |
Work item |
Status |
Acceptance criteria |
|---|---|---|---|
1 |
Prediction-valid masks and reset correctness |
Done |
Unspecified reset bits remain unknown to the mirror and are excluded from checks until predicted |
2 |
Passive register predictor |
Done |
Monitored reads/writes from any bus initiator update the correct generated handle with byte-enable diagnostics |
3 |
Generated backdoor metadata and adapters |
Done |
SystemRDL HDL paths can bind optional typed hierarchy access without affecting frontdoor-only users |
4 |
Register-file hierarchy and arrays |
Done |
Generated models retain natural indexed hierarchy and deterministic iteration |
5 |
Split and arbitrary-width frontdoors |
Done |
|
6 |
Typed field enumerations |
Done |
SystemRDL |
7 |
Block traversal and standard sequences |
Done |
Blocks expose reset/update/mirror traversal plus reusable reset, access, and bit-bash sequences |
8 |
Multiple maps, aliases, and custom frontdoors |
Done |
One logical model can represent alternate address views and exceptional access procedures |
9 |
User effects and RgGen import |
Done |
|
10 |
Optional access coverage and bulk memory operations |
Done |
Caller-owned bulk memory operations and opt-in access coverage add no unused-model cost |
Every row requires unit and negative tests, generated-model execution, a runnable usage example, and an exact pure-SystemVerilog performance peer before it is marked Done. See the register abstraction layer, register-generation guide, and sparse expected memory.
Rows 1-4 have their implementation, negative/generated tests, and exact
SystemVerilog peers. Row 5 supports split transfers, explicit
little/big-endian ordering, partial-failure accounting, and generated
Bits<Width> frontdoors for logical registers wider than 64 bits. The exact
register_split and register_wide C++/pure-SV semantic pairs pass at 100,000
iterations. Generated wide HDL backdoors, passive prediction over every
transfer address, and wide register-backed memory elements use the same typed
Bits<Width> model. The register_split and register_wide performance
entries are published as diagnostics (8.80x and 43.82x): their pure-SV
peers encode the register-model outcome – direct word moves and compares
– while the C++ side runs the typed model itself, so the pairs measure
the price of the abstraction layer rather than equivalent work.
register_coverage’s derived-work twin rewrite is the recorded template
for returning these rows to the hard gate.
Row 6 generates shared C++ enum types from SystemRDL encode, symbolic
failure diagnostics, typed read/write/stage/mirror access, and an explicit
.raw() escape hatch for reserved encodings. Its register_enum C++/pure-SV
semantic pair passes at 100,000 operations; its performance entry is a
diagnostic (10.21x) for the same reason as rows 5’s – the peer encodes
the outcome of the enum-typed access layer rather than running one.
Row 7 has generated reset_all(), update_all(), and mirror_all() traversal
in deterministic address order for homogeneous and arbitrary-width models. Its
optional sequence layer now provides reset-check, mixed frontdoor/backdoor
access-check, and conservative policy-aware bit-bash operations with generated
asynchronous traversal, summary counters, negative tests, and an exact pure-SV
semantic peer. The semantic pair passes at 100,000 iterations with 2,100,000
frontdoor operations and 4,600,002 checks. Formal timing remains pending an
admitted host-load window.
Row 8 provides named RegisterAddressMap instances with independent masters,
base addresses, per-register and per-memory aliases, and optional custom
frontdoors. All views share one logical staged/mirrored state; backdoor paths
continue to describe physical RTL storage. Its exact register_maps pair
executes the same primary, alias, and custom-frontdoor sequence in C++ and pure
SystemVerilog.
Row 9 provides an optional RegisterUserEffectPolicy for generated SystemRDL
ruser and wuser fields. Without a policy those bits deliberately become
unknown; with a policy, update encoding and read/write prediction are fully
defined. Native RgGen YAML, JSON, and TOML contracts can be imported directly
with cpptb-rggen, preserving hierarchy and path-qualified diagnostics. The
exact register_user_effects pair covers the policy hot path.
Packed field callbacks, cached register metadata, single-transfer prediction
commit, and a simulator-thread frame pool reduced the 10,000,000-iteration C++
diagnostic from 3.362 s to 0.994 s. The matching 0.400 s pure-SV median
gives a raw 2.49x ratio. A formal rerun was rejected before sampling because
host load exceeded admission limits. This zero-time abstraction-versus-inlined
equations microbenchmark is registered as diagnostic at 10,000,000 iterations;
the timed secworks AES integration remains the release-facing performance
comparison, and run-ahead work remains tracked in milestone 9.
Row 10 includes generated memory read/write(index, ..., AccessPath), raw
peek/poke, caller-owned bulk spans, partial-frontdoor failure reporting, and
typed SystemRDL hdl_path_slice mapping for one-dimensional HDL memories. Its
ergonomic layer includes allocation-free memory slices, compile-time register
array slices, typed register/field/memory traversal, and direct logical/HDL path
introspection. Entry-relative, memory-relative byte-offset, and absolute bus
address forms support both scalar and caller-owned chunk operations with
alignment and range diagnostics. RegisterAccessCoverage is an optional
analysis subscriber that records register/memory read and write outcomes,
including failed and unmapped transactions; models that do not instantiate it
execute no coverage path. Its
exact register_memory C++/pure-SV semantic pair performs the same four
deposits, four reads, six checks, and checksum updates per iteration. Optional
coverage has its own exact register_coverage peer under the ordinary
hard gate, certified at 0.2104x – the descriptor-driven C++ engine is
~4.75x faster than the equivalent derived-tally collector written in
SystemVerilog. Both sides precompute the register map’s decode into a
per-address action table at construction; the history of getting this
pair honest (including the answer-key twin it replaced) is recorded in
OPTIMIZATION_NOTES.
6. Interfaces, bidirectional signals, and portability¶
Status: In progress
Shipped in the Verilator reference flow:
generated named access for parameterized SystemVerilog interfaces with an explicit modport;
fixed interface arrays and independent clocks selected by exact generated signal identity;
drive(),high_z(), andget()for top-level and interface-memberinoutsignals, with no implicit delay;natural
dut.scope[index].memberaccess for homogeneous elaborated instance and generate arrays; andsimulator capability diagnostics that reject X/Z writes when a two-state backend would silently coerce them.
Two small remaining ergonomics items, both found by porting Ibex’s icache
testbench (experiments/open_core_ports/ports/ibex_icache_cpptb). Neither
changes any semantics; both are mechanical work that a project currently
repeats by hand:
Wide four-state ports. Code generation rejects any port wider than 32 bits whose declared type is four-state unless it is an
inout, for inputs and outputs alike and for interface members as well as top-level ports. The icache wrapper had to redeclare the 128-bit scrambling key and the 64-bit nonce asbitbefore it would build. Internal signals of the same width and type are unaffected, so this is a limit of the packed port transport rather than of the value model, and carrying the B plane for wide ports is the fix. Four-state values records the exact rule and diagnostic.A signal bundle for a flat pin list. A SystemVerilog interface port with a modport already generates a passable per-element group, so a driver and a monitor can share one
dut.link[index]handle. A design whose protocol pins are ordinary top-level ports has no equivalent. Every task in the icache port therefore takes the wholeDutand states which pins it owns only by which ones it happens to touch, and the wrapper module exists partly to lift the core-side, memory-side and key pins to the boundary where the UVM environment binds three interfaces instead.cpptb_vc’sApbBusshows the shape that works, an aggregate of generated signal handles constructed by the testbench, but one has to be written per protocol. The work is proportional to the protocol and repeated per port.
Remaining portability work:
Run the complete generated transport and scheduling conformance suite on at least one additional standards-compliant simulator. A four-state simulator is required to validate X/Z propagation end to end; Verilator remains the two-state performance reference. Simulator capability differences should be reported at startup rather than hidden in testbench code.
Structured logging now converts $realtime through seconds rather than
dividing by a 1fs literal, avoiding a portability bug where a coarser
timeprecision could round the divisor to zero. The Verilator integration test
asserts the exact femtosecond timestamps received through DPI. The same
assertion must pass when the second simulator joins the conformance matrix.
The framework now has a fail-closed Verilator capability gate and cached
semantic probe covering X/Z storage, high-impedance and conflicting-driver net
resolution, and both DPI directions. Verilator 5.050 and current upstream
development fail that probe;
their experimental --fourstate option is not a usable simulator capability
yet. A regression intentionally records this blocked state and will fail with
an enablement reminder when all semantics become available. The
four-state guide documents the existing LogicBits API and
the conformance criteria required before transport is enabled.
7. Debugging and release tooling¶
Status: In progress
Scoped structured logging is implemented with:
trace,debug,info,warning, anderrorlevels plusoff;lazy callable messages that are not invoked below the configured threshold;
test name, simulation time, source location, and lifecycle process provenance on every emitted record;
per-test sequence numbers and an opt-in owned
LogHistoryfor stable same-time ordering and sequential trace inspection;SystemVerilog logging macros that capture source location and hierarchy and merge HDL messages into the same test-owned sequence and history;
a synchronous harness-neutral
LogSinkthat does not retain messages inTestResult; andmatched
structured_loggingandstructured_log_historyC++/pure-SV pairs plus a mixed C++/RTL logging pair with a pure-SV peer.
The exact semantic pairs pass, and all three are certified under the
standard 1.10x guard: structured_logging at 0.5285x,
structured_log_history at 0.5044x, and the mixed C++/RTL pair at
0.7843x – below-threshold logging costs so little that the samples
needed two hundred million iterations to rise above the measurement
noise floor. See Structured logging for API and
sink-lifetime details.
Remaining facilities needed to diagnose and distribute real regressions:
optional machine-readable export of structured log histories;
transaction recording with component and process provenance;
Done: waveform dumping as a build variant –
--wave(FST or VCD), one file per test from the framework host loop on both timing backends, proven against the pure-SV twin by a cycle-sampled wave-equivalence gate inmake test, and proven across the two timing backends by a byte-identity gate: the same examples built per backend must produce identical result records and identical dumps (make backend-equivalence-test). Runtime start/stop windowing and scope selection remain open ergonomics on top;waveform-on-failure stays deliberately unautomated: waves are asked for, not implied by a failure;
Done: a scheduler wait graph with process parentage, spawn sites, outstanding triggers, named synchronization resources, timeout capture, and conservative deadlock classification;
clean attribution of HDL assertion failures;
simulator and generated-binding compatibility reports; and
versioned runtime and code-generator packages.
The wait-graph semantic, conformance, negative, and full-example regressions
pass. Its event hot-path performance guard is certified: the event
kernel passed the 1.10x gate at 0.8141x with the wait graph active.
Keep the core runtime header-only and the Slang-backed generator separately installable.
8. Coherent clock and reset control¶
Status: Planned
Extend clock ownership beyond initial registration. A returned clock handle should support stop, restart, pause, period or duty-cycle changes, and explicit release of ownership while remaining coherent with pending edge waits. The model must scale to unrelated input clocks and DUT-generated clocks, and report actionable ownership errors when a second component attempts to drive a scheduler-owned clock.
Reusable reset components may be built on these primitives, but their APIs must expose each asserted value and each wait. They must not silently drive a reset sequence merely because a test starts.
9. Batched execution and run-ahead experiments¶
Status: Planned
Profile the architectural options for reducing simulator-boundary resumptions
without weakening the ordinary coroutine API. The exact secworks AES workload
has measured 1.587x pure SV in a high-load diagnostic run even though its
generated clock produces no DPI callback. Its dominant remaining cost is
entering DPI and resuming the
C++ scheduler at each of 88,001 authored timing boundaries; register handles
and per-access coroutine wrappers account for only a few percent.
Prototype and measure these approaches independently:
a compact command batch that lets C++ submit several drives, waits, reads, and checks per DPI entry;
run-ahead execution that continues until a value-dependent read, unknown HDL event, cancellation point, or simulator phase requires control to return to C++; and
generated SystemVerilog execution of eligible batches while retaining the existing scheduler for dynamic or unsupported control flow.
The experiments must preserve arbitrary clocks, hierarchical signal access,
explicit timing, read-after-write behavior, cancellation, timeouts, process
ownership, and failure attribution. Batching must be an optimization beneath
the familiar Task authoring model or an explicitly scoped sequence facility,
not a second mandatory testbench language.
Each prototype must report DPI entries and scheduler resumptions as well as
wall time. It must first match an exact pure-SystemVerilog peer, then improve
the secworks AES benchmark and at least one realistic concurrent workload
without regressing existing workloads beyond the repository’s 1.10x policy.
Small register-handle and coroutine-wrapper changes are not priority
experiments because controlled decomposition shows they cannot close the
current gap.
10. Static hierarchy discovery¶
Status: Done
Shipped. The build recovers the access set by scanning compile-only object
sections – no link, no run – and clocks are registered at runtime by
start_clock() itself: the generated wrapper carries a driver task per
writable one-bit signal (including unpacked-array elements, which is how
interface-member clocks arrive) and queries the runtime after PHASE_INIT
for the ones actually started. Both failure modes below are gone, clocks.json
and its staleness checks are gone, and the executed-discovery entry point
survives only as an explicit opt-in (clock_discovery_source) for
manifest-driven flows. Validated by the example matrix under both timing
backends, the icache port on both backends with identical check counts, a
pin-exact core_ibex replay, and the directed 944 against the UVM baseline.
The behavior change step 2 called out is real and documented: cross-test
clock conflicts are now legal per-test configurations that fail at run time
if genuinely conflicting. The section below is kept as the original problem
statement and plan.
The build’s hierarchy-discovery pass compiles the testbench under
CPPTB_HIERARCHY_DISCOVERY and then runs it. Scoping measured what that
execution actually contributes, and the answer halves the problem: the access
set is already a static-initialization side effect of template instantiation —
nm over the compile-only objects reproduces access.json byte-identically
with no link and no run, verified on the examples and the Ibex core port. Only
the clock configuration comes from execution, which runs each registered
test’s prologue up to its first suspension.
Executing user code inside the build is still the defect. Measured, the
failure is worse than the hang this section first described: a value-dependent
loop before the first co_await does hang the build with a compiler command
as the last log line, but an edge wait nothing drives does not hang — it
suspends and yields a silently empty clock configuration, an undocumented
wrong answer. A timeout would rename the first symptom and never see the
second; this milestone removes the cause.
Done means the discovery outputs are produced without executing user test
code, both failure modes above are gone, and a testbench that builds today
builds identically with no source changes. An earlier version of this section
required keeping the transport’s “measured pruning benefit”; scoping found no
such measurement exists — ports were never value-pruned, and discovery prunes
hierarchy exports and edge observers. The real bar is output parity:
byte-identical access.json against the executed path, gated in CI until the
executed path is deleted. The steps, from the scoping report (STATIC-DISCOVERY-SCOPING.md at the repository root):
Access set without execution. Emit discovery records into a dedicated object section, compile the testbench translation units with
-c— no generated main, no link — and scan the objects. Same schema, parity-gated across GCC and Clang on the examples, benchmarks, and Ibex ports.Clocks without execution. Promote the already-implemented
dynamic_clocksmode to the default: the generated SV drivers query the runtime’s clock configuration at time zero over the existing DPI, andclocks.jsonwith its staleness checks disappears. The work is re-qualifying the benchmarks under the1.10xguard; the fallback if a guard fails is declared clocks through the existing--clockkey. One behavior change to call out: cross-test clock conflicts stop failing the build and become legal per-test configurations.Document the escape hatch. Explicit declaration through the pre-existing
--clock, edge-observer, and access-manifest keys as the override surface.
Rejected with reasons recorded in the scoping report: external static analysis (the compiler’s own instantiation is already the alias-robust oracle), a fuel-bounded dry run (still executes user code and contributes no access entries), and carrying every signal unpruned (hundreds of forced exports where the current builds need none, defeating simulator optimization).
No-priority backlog¶
Status: No priority
The following ideas are intentionally deferred and are not scheduled after the numbered milestones:
UCIS functional-coverage interchange;
additional protocol components such as AXI-Lite, UART, SPI, and richer streaming components;
Barrier, unless a concrete multi-process example needs semantics that cannot be expressed clearly withJoin,Event, orSemaphore;simulation state snapshot and restore, so a long run can branch or resume from a saved point instead of replaying from time zero — the analysis records the two viable shapes and the coroutine-frame constraint that rules out “checkpoint everything, mid-await”;
dynamic string-based hierarchy lookup; and
reference-harness enhancements including JUnit conversion, tag filtering, waveform-on-failure reruns, and reproduction-command presentation.
These items may be promoted only when a concrete project supplies the use case, semantics, runnable example, test coverage, and performance peer needed to justify prioritizing them.
Candidate directions¶
Analysis of capabilities the framework lacks, and of the open-source verification corpus needed to judge it against real alternatives, is recorded in future directions. Nothing there is scheduled: those items still have to clear the promotion bar described above.
One item there already has the use case, runnable example and performance peer that bar asks for, so it is named here rather than left to be found: aligning the scheduling semantics with cocotb. The trigger vocabulary matches cocotb deliberately, but writes apply immediately where cocotb defers them, so a cocotb testbench translated line for line drives a cycle early. The documented workaround is a convention rather than a mechanism: sample on the rising edge, drive off the falling one, and enter and leave every driving task at a drive point so that it does not re-anchor itself.
Porting Ibex’s icache testbench
(experiments/open_core_ports/ports/ibex_icache_cpptb) measured what holding
that convention costs. Every driving task had to be traced edge by edge against
the default output negedge clocking block it replaces, and the convention
itself is stated in a comment at the top of the file because nothing in the API
expresses it. It was the largest single cost of writing that port’s drivers.
This is the one real semantics gap the port produced; the two items it added to
milestone 6 are small
ergonomics work by comparison.
ReadWrite and ReadOnly express the convention directly, but no
cpptb build configuration supplies them to their documented contract. A
default build fails at run time; the --vpi stanza that the run-time message
names stops the failure without making the phases settle correctly; a project
can assemble a define combination that is wrong in a third way and says
nothing; and phases still only help an author who knows to reach for them.
Both contract-complete backends need cpptb to own the host loop, which means a
--cc --exe --build link against src/verilator_timing_main.cpp rather than
the --binary link cpptb build emits.
What the gap looks like¶
The cases below run against one design, phases: a 4 ns clock, a
combinational comb_sum = drive_value + addend, and one always_ff that
samples drive_value and counts edges. Measured under Verilator 5.050.
ReadWrite and ReadOnly fail at run time in a default cpptb build.
Task<void> driver(Dut dut, TestContext& test) {
dut.clk.set(0);
test.start_clock(dut.clk, 4_ns);
co_await RisingEdge{dut.clk};
co_await ReadWrite{};
dut.drive_value.set(0x55);
}
CPPTB_REGISTER_TEST(driver);
cpptb test reports only that the process produced nothing:
ERROR driver: simulator did not produce a result file
The reason is in build/cpptb/<name>/results/<test>.log:
CPP_DPI_PHASES_RESULT: ReadWrite, ReadOnly, and NextTimeStep need a timing
backend that dispatches simulator phases. This build has none: a default
`cpptb build` links Verilator's own --binary main, which owns clocks and
timers but dispatches no phases.
Wait on a clock edge instead: sample after `co_await RisingEdge{clk}`,
drive after `co_await FallingEdge{clk}`. That is the supported route and it
is what the ports in experiments/open_core_ports use.
Adding `verilator_args = ["--vpi"]` to cpptb.toml stops this error and
places writes on the right edge, but does NOT give a complete phase
contract: ReadOnly does not observe a write settled in ReadWrite. Only a
`--cc --exe --build` link against src/verilator_timing_main.cpp holds the
full contract, and `cpptb build` cannot produce one. See docs/scheduling.md.
The --vpi stanza the message describes lets the test run, and the drive
point it produces is right, but the phases are not contract-complete. Running the five checks of
timing_phase_contract from tests/conformance/runtime/testbench.cpp against
this design in a verilator_args = ["--vpi"] build fails two of them:
ReadOnly observes write settled after ReadWrite: actual=19 expected=35
ReadWrite observes sampled data after NBA: actual=16 expected=32
The same generated wrapper, testbench and RTL relinked as
--cc --exe --build --vpi against src/verilator_timing_main.cpp passes all
five: that main re-evaluates the model after each ReadWrite callback before
running ReadOnly, and Verilator’s own --binary main does not.
The drive-point convention. The icache port states the rule in a comment because nothing in the API does, and every driving task holds to it:
// A "drive point" is the instant just after a falling edge of clk_i. Every
// task below that drives a pin is entered at a drive point and returns at a
// drive point, and none of them opens with a wait: a task that re-anchored
// itself would place its first write one cycle later than the UVM driver
// does.
Task<void> wait_clks(Dut dut, uint32_t count) {
if (count == 0) co_return;
co_await clock_cycles(dut.clk_i, count);
co_await FallingEdge{dut.clk_i}; // return to a drive point
}
Task<void> lower_req(Dut dut, uint32_t cycles) {
if (cycles == 0) co_return;
dut.req_i.set(0); // already at a drive point: no wait
co_await wait_clks(dut, cycles);
dut.req_i.set(1);
}
Written against phases the same driver would carry no convention: it would not
matter where its caller was, and wait_clks would have nothing left to do.
Task<void> lower_req(Dut dut, uint32_t cycles) {
if (cycles == 0) co_return;
co_await ReadWrite{};
dut.req_i.set(0);
co_await clock_cycles(dut.clk_i, cycles);
co_await ReadWrite{};
dut.req_i.set(1);
}
Both shapes place a write on the edge after the point where the driver
resumed. On the design above, a driver that resumes on the 6 ns edge and then
awaits ReadWrite is captured by the 10 ns edge; the same driver writing
immediately after co_await RisingEdge{}, which is the line-for-line cocotb
translation, is captured by the 6 ns edge it just resumed on.
The silent wrong-answer configuration. Matching design.defines against
build.cxx_flags by hand selects the inline SV-DPI pump:
[design]
defines = ["CPPTB_SV_DPI_TIMING"]
[build]
cxx_flags = ["-DCPPTB_SV_DPI_TIMING"]
It builds and runs. Three of the five contract checks fail:
ReadOnly observes write settled after ReadWrite: actual=19 expected=35
ReadWrite observes clocked update in new timestep: actual=0 expected=1
ReadWrite observes sampled data after NBA: actual=0 expected=32
and the phase-form driver above drives a cycle early: its write is captured by the 6 ns edge rather than the 10 ns one. Nothing in the build or the run says so. Those failures are visible only because a probe asserts on them; a testbench that does not check settled values reports a pass.
Adding CPPTB_SV_DPI_NBA_TIMING (or additionally
CPPTB_SV_DPI_CALENDAR_TIMING) to the same hand-matched define pair selects
the NBA or calendar pump instead, and those pass all five contract checks and
place the driver’s write on the correct edge in a plain cpptb build. The
machinery for a contract-passing build therefore already exists behind
unvalidated defines. It is still not the full documented contract: with no
started clock and no framework timer, a NextTimeStep waiter under either
pump misses a DUT-internal #5ns event that direct dispatch and the VPI
bridge both wake for, and times out against the watchdog. That is the
observed-events limit the backend table
records.
Three ways to close it, in increasing order of commitment. They are not alternatives so much as a sequence: each is useful on its own, and each makes the next smaller.
Name and validate the backend selection — done.
[build] timing_backendaccepts exactly two names,"verilator-direct"and"vpi", both contract-complete; the policy is two supported backends and no more, because both sit at the privileged layer that sees the whole event queue, which the SV-DPI experiments cannot. The key emits the--cc --exe --build --vpilink against the framework host loop; a bare--vpiinverilator_argsand every hand-set timing define are rejected at resolve time with the key named; the run-time diagnostic in a backendless build names the key; andmake testconformance-checks both names on every run.Offer deferred writes, as a project mode — done.
deferred_writes = trueincpptb.tomlmakesset()itself carry cocotb’s semantics – queued, flushed at theReadWritesettle point the selected backend provides – withset_now()as the marked escape hatch, mirroring cocotb’ssetimmediatevalue(). There is noset_deferred()spelling: cocotb has one write syntax and so does this. The mode requirestiming_backend, enforced at build time, which keeps one mechanism instead of a default-build variant. Pinned semantics, matching cocotb exactly: aget()betweenset()and the flush returns the simulator’s current value, not the queued one.tests/integration/deferred_writespins that, write-lands-next-edge, settled-by-ReadOnly, and theset_now()escape hatch on both supported backends in everymake test. The port validation is done: the icache port converted to the mode passes ten of ten on both backends with check counts identical to the immediate-mode run, after three drive-point corrections documented in Coming from cocotb.Flip the mode’s default, then retire the immediate branches — flip done, retirement open. The default flipped:
timing_backenddefaults to"verilator-direct"anddeferred_writestotrue, applied at the resolution site so a backendless build cannot exist. Every project in the repository now builds on the mode — the 14 examples match their pure-SV twins on checks and cycle count, and the last pinned holdout,core_ibex_cpptb, converted with all 93 directed outcomes identical to its legacy-model run. What remains is the retirement:deferred_writes = falseis now legacy behavior on a deprecation path, not a supported long-term mode. Once it goes, the component library’s dual shape stops earning its keep:cpptb_vccarries an immediate-mode#elsebranch in every drive and observation path (falling-edge anchors, post-edgesample_delaysampling) solely for builds without the mode, and thesample_delayknobs exist only to serve that branch. The end state is one shape – rising-edge anchors, deferred flushes, pre-evaluation observation – with the immediate branches and their calibrated post-edge counting deleted, and the remaining immediate-mode consumers (the exact benchmark kernels and their pure-SV twins) migrated or re-calibrated in the same change. The benchmark peer the flip also waited for exists and is certified:timing_phases_deferred– the same kernel and pure-SV reference, built with the mode on – passes the1.10xguard at0.8903xagainst the SV twin, with the immediate kernel at0.7383xin a comparable admitted window. The write model’s measured cost on that worst case (two writes and three phase awaits per iteration, nothing else) is therefore about1.21xover immediate writes, while still beating the SV reference.
What 1 and 2 look like in use¶
Option 1 is implemented:
[build]
timing_backend = "verilator-direct" # or "vpi"
Option 2, under the mode, keeps one write spelling – a driver written the cocotb way is correct with no drive-point convention:
// Requires deferred_writes = true.
Task<void> driver(Dut dut) {
while (true) {
co_await RisingEdge{dut.clk};
dut.wdata.set(next_word()); // queued; lands for the next edge
dut.wvalid.set(1);
}
}
Doing 2 leaves the framework familiar to a cocotb author without changing what any existing testbench does. 3 is a separate decision and should be taken on its own evidence.
Deliberate non-goals¶
cpptb should not copy the complete UVM architecture. In particular, a factory, global configuration database, mandatory runtime phases, and objection system would add substantial machinery without improving the current testbench model. The useful ideas to retain are typed transactions and clear separation between sequences, drivers, monitors, and scoreboards.
That judgement now has a same-design comparison behind it, and it belongs
beside the gaps above so the trade is visible in one place. Ibex’s icache
testbench exists in both forms against the same DUT elaborated from the same 77
sources: upstream’s UVM environment in
experiments/open_core_ports/ports/ibex_icache_uvm, where all ten tests pass,
and a cpptb port of all ten in
experiments/open_core_ports/ports/ibex_icache_cpptb. The port needs no
factory, no configuration database, no phasing and no sequencer arbitration,
and its scoreboard is a plain class the monitor calls synchronously, which is
what an analysis port already provides. Generating stimulus values directly
also keeps a class of defect off the path entirely: the two icache ports
between them document six Verilator defects with reduced cases, five of them in
constrained randomization, and the cpptb port draws every field from
test.random() with no solver involved, so none of the five has anywhere to
occur. cpptb’s own optional constraint layer would put a solver back on the
path, but nothing obliges a testbench to use one. Wall time per test
is one to two hundred times lower, with the caveats recorded in the port’s
RESULTS.md: the baseline runs about 6% more cycles, logs at UVM_HIGH, and
elaborates the UVM environment and two protocol-checker modules that the port
does not, so it compares two harnesses rather than two simulators. The same
comparison now also exists at core level, where Ibex’s full directed testlist
reaches the identical 912-of-944 outcome on both harnesses;
Ports of real testbenches documents the whole program,
including its Spike co-simulation.
The first component library also does not require a heavyweight sequencer, mandatory agent hierarchy, or implicit component startup. Direct coroutine transactions and typed endpoints remain the default; arbitration is introduced only where a protocol or concurrent-caller use case requires it. The optional direct Z3 backend solves coupled constraints without making a solver a runtime dependency. A complete SystemVerilog constraint-language clone, dynamic random arrays, and coverage-guided solving remain deferred until real tests establish their value.
The optimized DPI path should also retain generated typed hierarchy access rather than adding runtime string-based signal lookup as the default. Dynamic lookup can remain a future portability or debugging facility if a concrete use case requires it.
The naming and capability comparisons are informed by cocotb’s official library reference, coroutine guide, and timing model. The transaction-component terminology follows the reusable portions of the Accellera UVM User’s Guide without adopting its full infrastructure. Generic protocol interfaces and shared memory models follow the useful separation demonstrated by the VUnit verification-component interfaces, while the randomization and coverage direction also considers OSVVM’s independently usable verification facilities.
Delivery process¶
For every runtime feature:
Add unit, negative, conformance, cancellation, and usage tests as applicable.
Add an individually runnable C++ DPI workload and exact SystemVerilog twin.
Run semantic parity before performance measurement.
Benchmark serially and stop on a valid C++ DPI/SystemVerilog ratio above
1.10xunless a scoped waiver is reviewed and recorded. Synthetic abstraction-versus-direct kernels may instead be explicitly classified as diagnostic when a matched timed integration benchmark retains the release gate.Run the full affected regression and retain a compact reviewed baseline.
Update the user guide, runnable examples, and roadmap status in the same change.