Fix event triggering (#6932)

This commit is contained in:
Igor Zaworski 2026-02-11 19:35:59 +01:00 committed by GitHub
parent e41436bd4a
commit 446bec3d1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 3765 additions and 1795 deletions

View File

@ -593,15 +593,17 @@ object.
This class manages processes that await events (triggers). There is one
such object per each trigger awaited by coroutines. Coroutines ``co_await``
this object's ``trigger`` function. They are stored in two stages -
`uncommitted` and `ready`. First, they land in the `uncommitted` stage, and
cannot be resumed. The ``resume`` function resumes all coroutines from the
`ready` stage and moves `uncommitted` coroutines into `ready`. The
``commit`` function only moves `uncommitted` coroutines into `ready`.
this object's ``trigger`` function. They are stored in three stages -
`awaiting`, `fired` and `toResume`. First, they land in the `awaiting` stage, and
cannot be resumed. The ``ready`` function moves all coroutines from the
`awaiting` stage into the `fired` stage. The ``moveToResumeQueue`` function moves
`fired` coroutines into `toResume`. Finally, function `resume` resumes
all coroutines from the `toResume` stage.
This split is done to avoid self-triggering and triggering coroutines
multiple times. See the `Scheduling with timing` section for details on how
this is used.
This split is done to avoid self-triggering, triggering coroutines
multiple times and triggering coroutines in the same iteration
they were suspended. See the `Scheduling with timing` section
for details on how this is used.
``VlDynamicTriggerScheduler``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -719,16 +721,35 @@ in that process. When ordering code using ``V3Order``, these triggers are
provided as external domains of these variables. This ensures that the
necessary combinational logic is triggered after a coroutine resumption.
Every call to a `VlTriggerScheduler`'s `trigger()` method is preempt by
a call to a proper `__VbeforeTrig` function which evaluates all the necessary
triggers so, the information about order of suspension/resumption is not lost.
The triggers necessary to evaluate are ones dependent on the same events
as the `trigger()` - e.g.: if `triggers()` awaits for event `a` or `b`, then
every trigger that depends on any of those shall be evaluated. If they wouldn't
be evaluated and next coroutine after resumption would fire the event `a` then
it is impossible to get to know whether await or fire on event `a` was called
first - which is necessary to know.
There are two functions for managing timing logic called by ``_eval()``:
* ``_timing_commit()``, which commits all coroutines whose triggers were
* ``_timing_ready()``, which commits all coroutines whose triggers were
not set in the current iteration,
* ``_timing_resume()``, which calls `resume()` on all trigger and delay
schedulers whose triggers were set in the current iteration.
Thanks to this separation, a coroutine awaiting a trigger cannot be
suspended and resumed in the same iteration, and it cannot be resumed
before it suspends.
Thanks to this separation a coroutine:
* awaiting a trigger cannot be suspended and resumed in the same iteration
(``test_regress/t/t_timing_eval_act.v``) - which is necessary to make
Verilator more predictable; this is the reason for introduction of 3rd stage
in `VlTriggerScheduler` and thanks to this it is guaranteed that downstream
logic will be evaluated before resumption (assuming that the coroutine wasn't
already triggered in previous iteration);
* cannot be resumed before it is suspended -
``test_regress/t/t_event_control_double_excessive.v``;
* firing cannot cannot be lost
(``test_regress/t/t_event_control_double_lost.v``) - which is possible when
triggers are not evaluated right before awaiting.
All coroutines are committed and resumed in the 'act' eval loop. With
timing features enabled, the ``_eval()`` function takes this form:

View File

@ -127,44 +127,65 @@ void VlTriggerScheduler::resume(const char* eventDescription) {
VL_DEBUG_IF(dump(eventDescription);
VL_DBG_MSGF(" Resuming processes waiting for %s\n", eventDescription););
#endif
std::swap(m_ready, m_resumeQueue);
for (VlCoroutineHandle& coro : m_resumeQueue) coro.resume();
m_resumeQueue.clear();
commit(eventDescription);
for (VlCoroutineHandle& coro : m_toResume) coro.resume();
m_toResume.clear();
}
void VlTriggerScheduler::commit(const char* eventDescription) {
void VlTriggerScheduler::moveToResumeQueue(const char* eventDescription) {
#ifdef VL_DEBUG
if (!m_uncommitted.empty()) {
if (!m_fired.empty()) {
VL_DEBUG_IF(VL_DBG_MSGF(" Moving to resume queue processes waiting for %s:\n",
eventDescription);
for (const auto& susp
: m_fired) {
VL_DBG_MSGF(" - ");
susp.dump();
});
}
#endif
std::swap(m_fired, m_toResume);
}
void VlTriggerScheduler::ready(const char* eventDescription) {
#ifdef VL_DEBUG
if (!m_awaiting.empty()) {
VL_DEBUG_IF(
VL_DBG_MSGF(" Committing processes waiting for %s:\n", eventDescription);
for (const auto& susp
: m_uncommitted) {
: m_awaiting) {
VL_DBG_MSGF(" - ");
susp.dump();
});
}
#endif
m_ready.reserve(m_ready.size() + m_uncommitted.size());
m_ready.insert(m_ready.end(), std::make_move_iterator(m_uncommitted.begin()),
std::make_move_iterator(m_uncommitted.end()));
m_uncommitted.clear();
const size_t expectedSize = m_fired.size() + m_awaiting.size();
if (m_fired.capacity() < expectedSize) m_fired.reserve(expectedSize * 2);
m_fired.insert(m_fired.end(), std::make_move_iterator(m_awaiting.begin()),
std::make_move_iterator(m_awaiting.end()));
m_awaiting.clear();
}
#ifdef VL_DEBUG
void VlTriggerScheduler::dump(const char* eventDescription) const {
if (m_ready.empty()) {
VL_DBG_MSGF(" No ready processes waiting for %s\n", eventDescription);
if (m_toResume.empty()) {
VL_DBG_MSGF(" No process to resume waiting for %s\n", eventDescription);
} else {
for (const auto& susp : m_ready) {
VL_DBG_MSGF(" Ready processes waiting for %s:\n", eventDescription);
for (const auto& susp : m_toResume) {
VL_DBG_MSGF(" Processes to resume waiting for %s:\n", eventDescription);
VL_DBG_MSGF(" - ");
susp.dump();
}
}
if (!m_uncommitted.empty()) {
VL_DBG_MSGF(" Uncommitted processes waiting for %s:\n", eventDescription);
for (const auto& susp : m_uncommitted) {
if (!m_fired.empty()) {
VL_DBG_MSGF(" Triggered processes waiting for %s:\n", eventDescription);
for (const auto& susp : m_awaiting) {
VL_DBG_MSGF(" - ");
susp.dump();
}
}
if (!m_awaiting.empty()) {
VL_DBG_MSGF(" Not triggered processes waiting for %s:\n", eventDescription);
for (const auto& susp : m_awaiting) {
VL_DBG_MSGF(" - ");
susp.dump();
}

View File

@ -233,38 +233,40 @@ public:
//=============================================================================
// VlTriggerScheduler stores coroutines to be resumed by a trigger. It does not keep track of its
// trigger, relying on calling code to resume when appropriate. Coroutines are kept in two stages
// - 'uncommitted' and 'ready'. Whenever a coroutine is suspended, it lands in the 'uncommitted'
// stage. Only when commit() is called, these coroutines get moved to the 'ready' stage. That's
// when they can be resumed. This is done to avoid resuming processes before they start waiting.
// trigger, relying on calling code to resume when appropriate. Coroutines are kept in three stages
// - 'awaiting', 'fired' and 'toResume'. Whenever a coroutine is suspended, it lands in the
// 'awaiting' stage. Only when ready() is called, these coroutines get moved to the 'fired' stage.
// When moveToResumeQueue() is begin called all coroutines from 'ready' are moved to 'toResume'.
// That's when they can be resumed. This is done to avoid resuming processes before they start
// waiting.
class VlTriggerScheduler final {
// TYPES
using VlCoroutineVec = std::vector<VlCoroutineHandle>;
// MEMBERS
VlCoroutineVec m_uncommitted; // Coroutines suspended before commit() was called
// (not resumable)
VlCoroutineVec m_ready; // Coroutines that can be resumed (all coros from m_uncommitted are
// moved here in commit())
VlCoroutineVec m_resumeQueue; // Coroutines being resumed by resume(); kept as a field to
// avoid reallocation. Resumed coroutines are moved to
// m_resumeQueue to allow adding coroutines to m_ready
// during resume(). Outside of resume() should always be empty.
VlCoroutineVec m_awaiting; // Coroutines suspended before ready() was called
// (not resumable)
VlCoroutineVec m_fired; // Coroutines that were triggered (all coros from m_awaiting are moved
// here in ready())
VlCoroutineVec m_toResume; // Coroutines to resume in next resumePrep()
// - moved here in commit()
public:
// METHODS
// Resumes all coroutines from the 'ready' stage
// Resumes all coroutines from the m_toResume
void resume(const char* eventDescription = VL_UNKNOWN);
// Moves all coroutines from m_uncommitted to m_ready
void commit(const char* eventDescription = VL_UNKNOWN);
// Moves all coroutines from m_fired to m_toResume
void moveToResumeQueue(const char* eventDescription = VL_UNKNOWN);
// Moves all coroutines from m_awaiting to m_fired
void ready(const char* eventDescription = VL_UNKNOWN);
// Are there no coroutines awaiting?
bool empty() const { return m_ready.empty() && m_uncommitted.empty(); }
bool empty() const { return m_fired.empty() && m_awaiting.empty(); }
#ifdef VL_DEBUG
void dump(const char* eventDescription) const;
#endif
// Used by coroutines for co_awaiting a certain trigger
auto trigger(bool commit, VlProcessRef process, const char* eventDescription = VL_UNKNOWN,
auto trigger(bool ready, VlProcessRef process, const char* eventDescription = VL_UNKNOWN,
const char* filename = VL_UNKNOWN, int lineno = 0) {
VL_DEBUG_IF(VL_DBG_MSGF(" Suspending process waiting for %s at %s:%d\n",
eventDescription, filename, lineno););
@ -279,8 +281,7 @@ public:
}
void await_resume() const {}
};
return Awaitable{commit ? m_ready : m_uncommitted, process,
VlFileLineDebug{filename, lineno}};
return Awaitable{ready ? m_fired : m_awaiting, process, VlFileLineDebug{filename, lineno}};
}
};

View File

@ -820,7 +820,9 @@ public:
RNG_SET_RANDSTATE,
SCHED_ANY_TRIGGERED,
SCHED_AWAITING_CURRENT_TIME,
SCHED_READY,
SCHED_COMMIT,
SCHED_MOVE_TO_RESUME_QUEUE,
SCHED_DELAY,
SCHED_DO_POST_UPDATES,
SCHED_ENQUEUE,
@ -950,7 +952,9 @@ inline std::ostream& operator<<(std::ostream& os, const VCMethod& rhs) {
{RNG_SET_RANDSTATE, "__Vm_rng.set_randstate", false}, \
{SCHED_ANY_TRIGGERED, "anyTriggered", false}, \
{SCHED_AWAITING_CURRENT_TIME, "awaitingCurrentTime", true}, \
{SCHED_READY, "ready", false}, \
{SCHED_COMMIT, "commit", false}, \
{SCHED_MOVE_TO_RESUME_QUEUE, "moveToResumeQueue", false}, \
{SCHED_DELAY, "delay", false}, \
{SCHED_DO_POST_UPDATES, "doPostUpdates", false}, \
{SCHED_ENQUEUE, "enqueue", false}, \

View File

@ -421,7 +421,7 @@ void createSettle(AstNetlist* netlistp, AstCFunc* const initFuncp, SenExprBuilde
// Gather the relevant sensitivity expressions and create the trigger kit
const auto& senTreeps = getSenTreesUsedBy({&comb, &hybrid});
const TriggerKit trigKit = TriggerKit::create(netlistp, initFuncp, senExprBulider, {},
senTreeps, "stl", extraTriggers, true);
senTreeps, "stl", extraTriggers, true, false);
// Remap sensitivities (comb has none, so only do the hybrid)
remapSensitivities(hybrid, trigKit.mapVec());
@ -446,7 +446,11 @@ void createSettle(AstNetlist* netlistp, AstCFunc* const initFuncp, SenExprBuilde
// Inner loop statements
nullptr,
// Prep statements: Compute the current 'stl' triggers
trigKit.newCompCall(),
[&trigKit] {
AstNodeStmt* const stmtp = trigKit.newCompBaseCall();
if (stmtp) stmtp->addNext(trigKit.newDumpCall(trigKit.vscp(), trigKit.name(), true));
return stmtp;
}(),
// Work statements: Invoke the 'stl' function
util::callVoidFunc(stlFuncp));
@ -499,7 +503,8 @@ AstNode* createInputCombLoop(AstNetlist* netlistp, AstCFunc* const initFuncp,
// Gather the relevant sensitivity expressions and create the trigger kit
const auto& senTreeps = getSenTreesUsedBy({&logic});
const TriggerKit trigKit = TriggerKit::create(netlistp, initFuncp, senExprBuilder, {},
senTreeps, "ico", extraTriggers, false);
senTreeps, "ico", extraTriggers, false, false);
std::ignore = senExprBuilder.getAndClearResults();
if (dpiExportTriggerVscp) {
trigKit.addExtraTriggerAssignment(dpiExportTriggerVscp, dpiExportTriggerIndex);
@ -549,7 +554,11 @@ AstNode* createInputCombLoop(AstNetlist* netlistp, AstCFunc* const initFuncp,
// Inner loop statements
nullptr,
// Prep statements: Compute the current 'ico' triggers
trigKit.newCompCall(),
[&trigKit] {
AstNodeStmt* const stmtp = trigKit.newCompBaseCall();
if (stmtp) stmtp->addNext(trigKit.newDumpCall(trigKit.vscp(), trigKit.name(), true));
return stmtp;
}(),
// Work statements: Invoke the 'ico' function
util::callVoidFunc(icoFuncp));
@ -586,8 +595,8 @@ void createEval(AstNetlist* netlistp, //
) {
FileLine* const flp = netlistp->fileline();
// 'createResume' consumes the contents that 'createCommit' needs, so do the right order
AstCCall* const timingCommitp = timingKit.createCommit(netlistp);
// 'createResume' consumes the contents that 'createReady' needs, so do the right order
AstCCall* const timingReadyp = timingKit.createReady(netlistp);
AstCCall* const timingResumep = timingKit.createResume(netlistp);
// Create the active eval loop
@ -598,9 +607,16 @@ void createEval(AstNetlist* netlistp, //
// Prep statements
[&]() {
// Compute the current 'act' triggers - the NBA triggers are the latched value
AstNodeStmt* stmtsp = trigKit.newCompCall(nbaKit.m_vscp);
// Commit trigger awaits from the previous iteration
if (timingCommitp) stmtsp = AstNode::addNext(stmtsp, timingCommitp->makeStmt());
AstNodeStmt* stmtsp = trigKit.newCompBaseCall();
AstNodeStmt* const dumpp
= stmtsp ? trigKit.newDumpCall(trigKit.vscp(), trigKit.name(), true) : nullptr;
// Mark as ready for triggered awaits
if (timingReadyp) stmtsp = AstNode::addNext(stmtsp, timingReadyp->makeStmt());
if (AstVarScope* const vscAccp = trigKit.vscAccp()) {
stmtsp = AstNode::addNext(stmtsp, trigKit.newOrIntoCall(actKit.m_vscp, vscAccp));
}
stmtsp = AstNode::addNext(stmtsp, trigKit.newCompExtCall(nbaKit.m_vscp));
stmtsp = AstNode::addNext(stmtsp, dumpp);
// Latch the 'act' triggers under the 'nba' triggers
stmtsp = AstNode::addNext(stmtsp, trigKit.newOrIntoCall(nbaKit.m_vscp, actKit.m_vscp));
//
@ -609,8 +625,15 @@ void createEval(AstNetlist* netlistp, //
// Work statements
[&]() {
AstNodeStmt* workp = nullptr;
if (AstVarScope* const actAccp = trigKit.vscAccp()) {
AstCMethodHard* const cCallp = new AstCMethodHard{
flp, new AstVarRef{flp, actAccp, VAccess::WRITE}, VCMethod::UNPACKED_FILL,
new AstConst{flp, AstConst::Unsized64{}, 0}};
cCallp->dtypeSetVoid();
workp = AstNode::addNext(workp, cCallp->makeStmt());
}
// Resume triggered timing schedulers
if (timingResumep) workp = timingResumep->makeStmt();
if (timingResumep) workp = AstNode::addNext(workp, timingResumep->makeStmt());
// Invoke the 'act' function
workp = AstNode::addNext(workp, util::callVoidFunc(actKit.m_funcp));
//
@ -650,7 +673,7 @@ void createEval(AstNetlist* netlistp, //
netlistp->nbaEventp(nullptr);
netlistp->nbaEventTriggerp(nullptr);
// If a dynamic NBA is pending, clear the pending flag and fire the commit event
// If a dynamic NBA is pending, clear the pending flag and fire the ready event
AstIf* const ifp = new AstIf{flp, new AstVarRef{flp, nbaEventTriggerp, VAccess::READ}};
ifp->addThensp(util::setVar(continuep, 1));
ifp->addThensp(util::setVar(nbaEventTriggerp, 0));
@ -867,11 +890,12 @@ void schedule(AstNetlist* netlistp) {
&logicRegions.m_obs, //
&logicRegions.m_react, //
&timingKit.m_lbs});
const TriggerKit trigKit = TriggerKit::create(netlistp, staticp, senExprBuilder, preTreeps,
senTreeps, "act", extraTriggers, false);
const TriggerKit trigKit
= TriggerKit::create(netlistp, staticp, senExprBuilder, preTreeps, senTreeps, "act",
extraTriggers, false, v3Global.usesTiming());
// Add post updates from the timing kit
if (timingKit.m_postUpdates) trigKit.compp()->addStmtsp(timingKit.m_postUpdates);
if (timingKit.m_postUpdates) trigKit.compBasep()->addStmtsp(timingKit.m_postUpdates);
if (dpiExportTriggerVscp) {
trigKit.addExtraTriggerAssignment(dpiExportTriggerVscp, dpiExportTriggerIndex);
@ -996,7 +1020,44 @@ void schedule(AstNetlist* netlistp) {
createEval(netlistp, icoLoopp, trigKit, actKit, nbaKit, obsKit, reactKit, postponedFuncp,
timingKit);
// Step 15: Clean up
// Step 15: Add neccessary evaluation before awaits
if (AstCCall* const readyp = timingKit.createReady(netlistp)) {
staticp->addStmtsp(readyp->makeStmt());
beforeTrigVisitor(netlistp, senExprBuilder, trigKit);
} else {
// beforeTrigVisitor clears Sentree pointers in AstCAwaits (as these sentrees will get
// deleted later) if there was no need to call it, SenTrees have to be cleaned manually
netlistp->foreach([](AstCAwait* const cAwaitp) { cAwaitp->clearSentreep(); });
}
if (AstVarScope* const trigAccp = trigKit.vscAccp()) {
// Copy trigger vector to accumulator at the end of static initialziation so,
// triggers fired during initialization persist to the first resume.
const AstUnpackArrayDType* const trigAccDTypep
= VN_AS(trigAccp->dtypep(), UnpackArrayDType);
UASSERT_OBJ(
trigAccDTypep->right() == 0, trigAccp,
"Expected that trigger vector and accumulator start elements enumeration from 0");
UASSERT_OBJ(trigAccDTypep->left() >= 0, trigAccp,
"Expected that trigger vector and accumulator has no negative indexes");
FileLine* const flp = trigAccp->fileline();
AstVarScope* const vscp = netlistp->topScopep()->scopep()->createTemp("__Vi", 32);
AstLoop* const loopp = new AstLoop{flp};
loopp->addStmtsp(
new AstAssign{flp,
new AstArraySel{flp, new AstVarRef{flp, trigAccp, VAccess::WRITE},
new AstVarRef{flp, vscp, VAccess::READ}},
new AstArraySel{flp, new AstVarRef{flp, actKit.m_vscp, VAccess::READ},
new AstVarRef{flp, vscp, VAccess::READ}}});
loopp->addStmtsp(util::incrementVar(vscp));
loopp->addStmtsp(new AstLoopTest{
flp, loopp,
new AstLte{flp, new AstVarRef{flp, vscp, VAccess::READ},
new AstConst{flp, AstConst::WidthedValue{}, 32,
static_cast<uint32_t>(trigAccDTypep->left())}}});
staticp->addStmtsp(loopp);
}
// Step 16: Clean up
netlistp->clearStlFirstIterationp();
// Haven't split static initializer yet

View File

@ -184,10 +184,12 @@ struct LogicReplicas final {
// "Extra" triggers, with "Sense" triggers taking up the bulk of the bits.
//
class TriggerKit final {
public:
// Triggers are storead as an UnpackedArray with a fixed word size
static constexpr uint32_t WORD_SIZE_LOG2 = 6; // 64-bits / VL_QUADSIZE
static constexpr uint32_t WORD_SIZE = 1 << WORD_SIZE_LOG2;
private:
const std::string m_name; // TriggerKit name
const bool m_slow; // TriggerKit is for schedulign 'slow' code
const uint32_t m_nSenseWords; // Number of words for Sense triggers
@ -195,6 +197,8 @@ class TriggerKit final {
const uint32_t m_nPreWords; // Number of words for 'pre' part
const uint32_t m_nVecWords = m_nSenseWords + m_nExtraWords; // Number of words in 'vec' part
// SenItems to corresponding bit indexes
std::unordered_map<VNRef<const AstSenItem>, size_t> m_senItem2TrigIdx;
// Data type of a single trigger word
AstNodeDType* m_wordDTypep = nullptr;
// Data type of a trigger vector holding one copy of all triggers
@ -204,8 +208,14 @@ class TriggerKit final {
AstUnpackArrayDType* m_trigExtDTypep = nullptr;
// The AstVarScope representing the extended trigger vector
AstVarScope* m_vscp = nullptr;
// The AstCFunc that computes the current active triggers
AstCFunc* m_compp = nullptr;
// The AstVarScope representing the trigger accumulator vector
// It is used to accumulate triggers that were found fired and cleared in beforeTrigger's
// in current 'act' region iteration
AstVarScope* m_vscAccp = nullptr;
// The AstCFunc that computes the current active base triggers
AstCFunc* m_compVecp = nullptr;
// The AstCFunc that computes the current active extended triggers
AstCFunc* m_compExtp = nullptr;
// The AstCFunc that dumps a trigger vector
AstCFunc* m_dumpp = nullptr;
// The AstCFunc that dumps an exended trigger vector - create lazily
@ -214,8 +224,7 @@ class TriggerKit final {
mutable AstCFunc* m_anySetVecp = nullptr;
mutable AstCFunc* m_anySetExtp = nullptr;
// The AstCFunc setting bits in a trigger vector that are set in another - create lazily
mutable AstCFunc* m_orIntoVecp = nullptr;
mutable AstCFunc* m_orIntoExtp = nullptr;
mutable std::array<AstCFunc*, 4> m_orIntoVecps = {nullptr};
// The AstCFunc setting a trigger vector to all zeroes - create lazily
mutable AstCFunc* m_clearp = nullptr;
@ -228,13 +237,15 @@ class TriggerKit final {
AstCFunc* createDumpExtFunc() const;
AstCFunc* createAnySetFunc(AstUnpackArrayDType* const dtypep) const;
AstCFunc* createClearFunc() const;
AstCFunc* createOrIntoFunc(AstUnpackArrayDType* const iDtypep) const;
AstCFunc* createOrIntoFunc(AstUnpackArrayDType* const oDtypep,
AstUnpackArrayDType* const iDtypep) const;
// Create an AstSenTree that is sensitive to the given trigger indices
AstSenTree* newTriggerSenTree(AstVarScope* vscp, const std::vector<uint32_t>& indices) const;
TriggerKit(const std::string& name, bool slow, uint32_t nSenseWords, uint32_t nExtraWords,
uint32_t nPreWords);
uint32_t nPreWords,
std::unordered_map<VNRef<const AstSenItem>, size_t> senItem2TrigIdx, bool useAcc);
VL_UNCOPYABLE(TriggerKit);
TriggerKit& operator=(TriggerKit&&) = delete;
@ -258,6 +269,9 @@ public:
}
uint32_t size() const { return m_descriptions.size(); }
};
// Generates list of assignments that fills
static AstAssign* createSenTrigVecAssignment(AstVarScope* const target,
std::vector<AstNodeExpr*>& trigps);
// Create a TriggerKit for the given AstSenTree vector
static TriggerKit create(AstNetlist* netlistp, //
@ -267,11 +281,17 @@ public:
const std::vector<const AstSenTree*>& senTreeps, //
const string& name, //
const ExtraTriggers& extraTriggers, //
bool slow);
bool slow, //
bool useAcc);
// ACCESSORS
AstVarScope* vscp() const { return m_vscp; }
AstCFunc* compp() const { return m_compp; }
AstVarScope* vscAccp() const { return m_vscAccp; }
size_t senItem2TrigIdx(const AstSenItem* senItemp) const {
return m_senItem2TrigIdx.at(*senItemp);
}
AstCFunc* compBasep() const { return m_compVecp; }
const std::string& name() const { return m_name; }
const std::unordered_map<const AstSenTree*, AstSenTree*>& mapPre() const { return m_mapPre; }
const std::unordered_map<const AstSenTree*, AstSenTree*>& mapVec() const { return m_mapVec; }
@ -281,7 +301,8 @@ public:
AstNodeStmt* newClearCall(AstVarScope* vscp) const;
AstNodeStmt* newOrIntoCall(AstVarScope* op, AstVarScope* ip) const;
// Helpers for code generation
AstNodeStmt* newCompCall(AstVarScope* vscp = nullptr) const;
AstNodeStmt* newCompBaseCall() const;
AstNodeStmt* newCompExtCall(AstVarScope* vscp) const;
AstNodeStmt* newDumpCall(AstVarScope* vscp, const std::string& tag, bool debugOnly) const;
// Create a new (non-extended) trigger vector - might return nullptr if there are no triggers
AstVarScope* newTrigVec(const std::string& name) const;
@ -296,7 +317,7 @@ public:
// Everything needed for combining timing with static scheduling.
class TimingKit final {
AstCFunc* m_resumeFuncp = nullptr; // Global timing resume function
AstCFunc* m_commitFuncp = nullptr; // Global timing commit function
AstCFunc* m_readyFuncp = nullptr; // Global timing ready function
// Additional var sensitivities for V3Order
std::map<const AstVarScope*, std::set<AstSenTree*>> m_externalDomains;
@ -310,8 +331,8 @@ public:
const std::unordered_map<const AstSenTree*, AstSenTree*>& trigMap) const VL_MT_DISABLED;
// Creates a timing resume call (if needed, else returns null)
AstCCall* createResume(AstNetlist* const netlistp) VL_MT_DISABLED;
// Creates a timing commit call (if needed, else returns null)
AstCCall* createCommit(AstNetlist* const netlistp) VL_MT_DISABLED;
// Creates a timing ready call (if needed, else returns null)
AstCCall* createReady(AstNetlist* const netlistp) VL_MT_DISABLED;
TimingKit() = default;
TimingKit(LogicByScope&& lbs, AstNodeStmt* postUpdates,
@ -403,6 +424,9 @@ void splitCheck(AstCFunc* ofuncp);
AstIf* createIfFromSenTree(AstSenTree* senTreep);
} // namespace util
void beforeTrigVisitor(AstNetlist* netlistp, SenExprBuilder& senExprBuilder,
const TriggerKit& trigKit);
} // namespace V3Sched
#endif // Guard

View File

@ -16,7 +16,7 @@
//
// Functions defined in this file are used by V3Sched.cpp to properly integrate
// static scheduling with timing features. They create external domains for
// variables, remap them to trigger vectors, and create timing resume/commit
// variables, remap them to trigger vectors, and create timing resume/ready
// calls for the global eval loop. There is also a function that transforms
// forks into emittable constructs.
//
@ -68,6 +68,24 @@ AstCCall* TimingKit::createResume(AstNetlist* const netlistp) {
m_resumeFuncp->declPrivate(true);
scopeTopp->addBlocksp(m_resumeFuncp);
for (const auto& p : m_lbs) {
AstActive* const activep = p.second;
activep->foreach([this](AstCMethodHard* const exprp) {
if (exprp->method() != VCMethod::SCHED_RESUME) return;
AstNodeExpr* const fromp = exprp->fromp();
if (VN_AS(fromp->dtypep(), BasicDType)->keyword()
!= VBasicDTypeKwd::TRIGGER_SCHEDULER) {
return;
}
AstCMethodHard* const moveToResumep = new AstCMethodHard{
fromp->fileline(), fromp->cloneTree(false),
VCMethod::SCHED_MOVE_TO_RESUME_QUEUE,
exprp->pinsp() ? exprp->pinsp()->cloneTree(true) : nullptr};
moveToResumep->dtypeSetVoid();
m_resumeFuncp->addStmtsp(moveToResumep->makeStmt());
});
}
// Put all the timing actives in the resume function
AstIf* dlyShedIfp = nullptr;
for (auto& p : m_lbs) {
@ -77,13 +95,12 @@ AstCCall* TimingKit::createResume(AstNetlist* const netlistp) {
AstVarRef* const schedrefp = VN_AS(
VN_AS(VN_AS(activep->stmtsp(), StmtExpr)->exprp(), CMethodHard)->fromp(), VarRef);
AstIf* const ifp = V3Sched::util::createIfFromSenTree(activep->sentreep());
ifp->addThensp(activep->stmtsp()->unlinkFrBackWithNext());
AstNode* const actionp = activep->stmtsp()->unlinkFrBackWithNext();
if (schedrefp->varScopep()->dtypep()->basicp()->isDelayScheduler()) {
dlyShedIfp = ifp;
dlyShedIfp = V3Sched::util::createIfFromSenTree(activep->sentreep());
dlyShedIfp->addThensp(actionp);
} else {
m_resumeFuncp->addStmtsp(ifp);
m_resumeFuncp->addStmtsp(actionp);
}
}
if (dlyShedIfp) m_resumeFuncp->addStmtsp(dlyShedIfp);
@ -97,10 +114,10 @@ AstCCall* TimingKit::createResume(AstNetlist* const netlistp) {
}
//============================================================================
// Creates a timing commit call (if needed, else returns null)
// Creates a timing ready call (if needed, else returns null)
AstCCall* TimingKit::createCommit(AstNetlist* const netlistp) {
if (!m_commitFuncp) {
AstCCall* TimingKit::createReady(AstNetlist* const netlistp) {
if (!m_readyFuncp) {
for (auto& p : m_lbs) {
AstActive* const activep = p.second;
auto* const resumep = VN_AS(VN_AS(activep->stmtsp(), StmtExpr)->exprp(), CMethodHard);
@ -111,67 +128,35 @@ AstCCall* TimingKit::createCommit(AstNetlist* const netlistp) {
|| schedulerp->dtypep()->basicp()->isDynamicTriggerScheduler(),
schedulerp, "Unexpected type");
if (!schedulerp->dtypep()->basicp()->isTriggerScheduler()) continue;
// Create the global commit function only if we have trigger schedulers
if (!m_commitFuncp) {
// Create the global ready function only if we have trigger schedulers
if (!m_readyFuncp) {
AstScope* const scopeTopp = netlistp->topScopep()->scopep();
m_commitFuncp
= new AstCFunc{netlistp->fileline(), "_timing_commit", scopeTopp, ""};
m_commitFuncp->dontCombine(true);
m_commitFuncp->isLoose(true);
m_commitFuncp->isConst(false);
m_commitFuncp->declPrivate(true);
scopeTopp->addBlocksp(m_commitFuncp);
m_readyFuncp = new AstCFunc{netlistp->fileline(), "_timing_ready", scopeTopp, ""};
m_readyFuncp->dontCombine(true);
m_readyFuncp->isLoose(true);
m_readyFuncp->isConst(false);
m_readyFuncp->declPrivate(true);
scopeTopp->addBlocksp(m_readyFuncp);
}
// There is a somewhat complicate dance here. Given a suspendable
// process of the form:
// ->evntA;
// $display("Fired evntA");
// @(evntA or evntB);
// The firing of the event cannot trigger the event control
// following it, as the process is not yet sensitive to the event
// when it fires (same applies for change detects). The way the
// scheduling works, the @evnt will suspend the process before
// the firing of the event is recognized on the next iteration of
// the 'act' loop, and hence could incorrectly resume the @evnt
// statement. To make this work, whenever a process suspends, it
// goes into an "uncommitted" state, so it cannot be resumed
// immediately on the next iteration of the 'act' loop, which is
// what we want. The question then is, when should the suspended
// process be "committed" and hence possible to be resumed. This is
// done when it is know for sure the suspending expression was not
// triggered on the current iteration of the 'act' loop. With
// multiple events in the suspending expression, all events need
// to be not triggered to safely commit the suspended process.
//
// This is is consistent with IEEE scheduling semantics, and
// behaves as if the above was executed as:
// ->evntA;
// $display("Fired evnt");
// ... all other statements in the 'act' loop that might fire evntA or evntB ...
// @(evntA or evntB);
// which is a valid execution. Race conditions be fun to debug,
// but they are a responsibility of the user.
AstSenTree* const senTreep = activep->sentreep();
FileLine* const flp = senTreep->fileline();
// Create an 'AstIf' sensitive to the suspending triggers
AstIf* const ifp = V3Sched::util::createIfFromSenTree(senTreep);
m_commitFuncp->addStmtsp(ifp);
m_readyFuncp->addStmtsp(ifp);
// Commit the processes suspended on this sensitivity expression
// in the **else** branch, when the event is known to be not fired.
// Mark as ready the processes resumed on this sensitivity expression
AstVarRef* const refp = new AstVarRef{flp, schedulerp, VAccess::READWRITE};
AstCMethodHard* const callp = new AstCMethodHard{flp, refp, VCMethod::SCHED_COMMIT};
AstCMethodHard* const callp = new AstCMethodHard{flp, refp, VCMethod::SCHED_READY};
callp->dtypeSetVoid();
if (resumep->pinsp()) callp->addPinsp(resumep->pinsp()->cloneTree(false));
ifp->addElsesp(callp->makeStmt());
ifp->addThensp(callp->makeStmt());
}
// We still haven't created a commit function (no trigger schedulers), return null
if (!m_commitFuncp) return nullptr;
// We still haven't created a ready function (no trigger schedulers), return null
if (!m_readyFuncp) return nullptr;
}
AstCCall* const callp = new AstCCall{m_commitFuncp->fileline(), m_commitFuncp};
AstCCall* const callp = new AstCCall{m_readyFuncp->fileline(), m_readyFuncp};
callp->dtypeSetVoid();
return callp;
}
@ -223,7 +208,7 @@ class AwaitVisitor final : public VNVisitor {
if (schedulerp->dtypep()->basicp()->isTriggerScheduler()) {
UASSERT_OBJ(methodp->pinsp(), methodp,
"Trigger method should have pins from V3Timing");
// The first pin is the commit boolean, the rest (if any) should be debug info
// The first pin is the ready boolean, the rest (if any) should be debug info
// See V3Timing for details
if (AstNode* const dbginfop = methodp->pinsp()->nextp()) {
if (methodp->pinsp()) addResumePins(resumep, static_cast<AstNodeExpr*>(dbginfop));
@ -267,7 +252,6 @@ class AwaitVisitor final : public VNVisitor {
void visit(AstCAwait* nodep) override {
if (AstSenTree* const sentreep = nodep->sentreep()) {
if (!sentreep->user1SetOnce()) createResumeActive(nodep);
nodep->clearSentreep(); // Clear as these sentrees will get deleted later
if (m_inProcess) m_processDomains.insert(sentreep);
}
}

View File

@ -225,19 +225,21 @@ AstCFunc* TriggerKit::createClearFunc() const {
// Done
return funcp;
}
AstCFunc* TriggerKit::createOrIntoFunc(AstUnpackArrayDType* const iDtypep) const {
AstCFunc* TriggerKit::createOrIntoFunc(AstUnpackArrayDType* const oDtypep,
AstUnpackArrayDType* const iDtypep) const {
AstNetlist* const netlistp = v3Global.rootp();
FileLine* const flp = netlistp->topScopep()->fileline();
AstNodeDType* const u32DTypep = netlistp->findUInt32DType();
// Create function
std::string name = "_trigger_orInto__" + m_name;
name += iDtypep == m_trigVecDTypep ? "" : "_ext";
name += iDtypep == m_trigVecDTypep ? "_vec" : "_ext";
name += oDtypep == m_trigVecDTypep ? "_vec" : "_ext";
AstCFunc* const funcp = util::makeSubFunction(netlistp, name, m_slow);
funcp->isStatic(true);
// Add arguments
AstVarScope* const oVscp = newArgument(funcp, m_trigVecDTypep, "out", VDirection::INOUT);
AstVarScope* const oVscp = newArgument(funcp, oDtypep, "out", VDirection::INOUT);
AstVarScope* const iVscp = newArgument(funcp, iDtypep, "in", VDirection::CONSTREF);
// Add loop counter variable
@ -257,10 +259,14 @@ AstCFunc* TriggerKit::createOrIntoFunc(AstUnpackArrayDType* const iDtypep) const
AstNodeExpr* const oWordp = new AstArraySel{flp, rd(oVscp), rd(nVscp)};
AstNodeExpr* const iWordp = new AstArraySel{flp, rd(iVscp), rd(nVscp)};
AstNodeExpr* const rhsp = new AstOr{flp, oWordp, iWordp};
AstNodeExpr* const limp = new AstConst{flp, AstConst::WidthedValue{}, 32, m_nVecWords};
AstConst* const outputRangeLeftp = VN_AS(oDtypep->rangep()->leftp(), Const);
AstConst* const inputRangeLeftp = VN_AS(iDtypep->rangep()->leftp(), Const);
AstNodeExpr* const limp = outputRangeLeftp->num().toSInt() < inputRangeLeftp->num().toSInt()
? outputRangeLeftp->cloneTreePure(false)
: inputRangeLeftp->cloneTreePure(false);
loopp->addStmtsp(new AstAssign{flp, lhsp, rhsp});
loopp->addStmtsp(util::incrementVar(nVscp));
loopp->addStmtsp(new AstLoopTest{flp, loopp, new AstLt{flp, rd(nVscp), limp}});
loopp->addStmtsp(new AstLoopTest{flp, loopp, new AstLte{flp, rd(nVscp), limp}});
// Done
return funcp;
@ -297,16 +303,16 @@ AstNodeStmt* TriggerKit::newClearCall(AstVarScope* const vscp) const {
}
AstNodeStmt* TriggerKit::newOrIntoCall(AstVarScope* const oVscp, AstVarScope* const iVscp) const {
if (!m_nVecWords) return nullptr;
UASSERT_OBJ(oVscp->dtypep() == m_trigVecDTypep, oVscp, "Bad trigger vector type");
AstCFunc* funcp = nullptr;
if (iVscp->dtypep() == m_trigVecDTypep) {
if (!m_orIntoVecp) m_orIntoVecp = createOrIntoFunc(m_trigVecDTypep);
funcp = m_orIntoVecp;
} else if (iVscp->dtypep() == m_trigExtDTypep) {
if (!m_orIntoExtp) m_orIntoExtp = createOrIntoFunc(m_trigExtDTypep);
funcp = m_orIntoExtp;
} else {
iVscp->v3fatalSrc("Bad trigger vector type");
UASSERT_OBJ(iVscp->dtypep() == m_trigVecDTypep || iVscp->dtypep() == m_trigExtDTypep, iVscp,
"Bad input trigger vector type");
UASSERT_OBJ(oVscp->dtypep() == m_trigVecDTypep || oVscp->dtypep() == m_trigExtDTypep, oVscp,
"Bad output trigger vector type");
const size_t mask
= ((oVscp->dtypep() == m_trigExtDTypep) << 1) | (iVscp->dtypep() == m_trigExtDTypep);
AstCFunc*& funcp = m_orIntoVecps[mask];
if (!funcp) {
funcp = createOrIntoFunc(VN_AS(oVscp->dtypep(), UnpackArrayDType),
VN_AS(iVscp->dtypep(), UnpackArrayDType));
}
FileLine* const flp = v3Global.rootp()->topScopep()->fileline();
AstCCall* const callp = new AstCCall{flp, funcp};
@ -316,13 +322,19 @@ AstNodeStmt* TriggerKit::newOrIntoCall(AstVarScope* const oVscp, AstVarScope* co
return callp->makeStmt();
}
AstNodeStmt* TriggerKit::newCompCall(AstVarScope* vscp) const {
AstNodeStmt* TriggerKit::newCompBaseCall() const {
if (!m_nVecWords) return nullptr;
// If there are pre triggers, we need the argument
UASSERT(!m_nPreWords || vscp, "Need latched values for pre trigger compute");
FileLine* const flp = v3Global.rootp()->topScopep()->fileline();
AstCCall* const callp = new AstCCall{flp, m_compp};
if (m_nPreWords) callp->addArgsp(new AstVarRef{flp, vscp, VAccess::READ});
AstCCall* const callp = new AstCCall{flp, m_compVecp};
callp->dtypeSetVoid();
return callp->makeStmt();
}
AstNodeStmt* TriggerKit::newCompExtCall(AstVarScope* vscp) const {
if (!m_nPreWords) return nullptr;
FileLine* const flp = v3Global.rootp()->topScopep()->fileline();
AstCCall* const callp = new AstCCall{flp, m_compExtp};
callp->addArgsp(new AstVarRef{flp, vscp, VAccess::READ});
callp->dtypeSetVoid();
return callp->makeStmt();
}
@ -402,24 +414,28 @@ void TriggerKit::addExtraTriggerAssignment(AstVarScope* vscp, uint32_t index, bo
AstNodeExpr* const wordp = new AstArraySel{flp, refp, static_cast<int>(wordIndex)};
AstNodeExpr* const trigLhsp = new AstSel{flp, wordp, static_cast<int>(bitIndex), 1};
AstNodeExpr* const trigRhsp = new AstVarRef{flp, vscp, VAccess::READ};
AstNodeStmt* const setp = new AstAssign{flp, trigLhsp, trigRhsp};
AstNode* const setp = new AstAssign{flp, trigLhsp, trigRhsp};
if (clear) {
// Clear the input variable
AstNodeStmt* const clrp = new AstAssign{flp, new AstVarRef{flp, vscp, VAccess::WRITE},
new AstConst{flp, AstConst::BitFalse{}}};
// Note these are added in reverse order, so 'setp' executes before 'clrp'
m_compp->stmtsp()->addHereThisAsNext(clrp);
setp->addNext(new AstAssign{flp, new AstVarRef{flp, vscp, VAccess::WRITE},
new AstConst{flp, AstConst::BitFalse{}}});
}
m_compp->stmtsp()->addHereThisAsNext(setp);
if (AstNode* const nodep = m_compVecp->stmtsp()) {
setp->addNext(setp, nodep->unlinkFrBackWithNext());
}
m_compVecp->addStmtsp(setp);
}
TriggerKit::TriggerKit(const std::string& name, bool slow, uint32_t nSenseWords,
uint32_t nExtraWords, uint32_t nPreWords)
uint32_t nExtraWords, uint32_t nPreWords,
std::unordered_map<VNRef<const AstSenItem>, size_t> senItem2TrigIdx,
bool useAcc)
: m_name{name}
, m_slow{slow}
, m_nSenseWords{nSenseWords}
, m_nExtraWords{nExtraWords}
, m_nPreWords{nPreWords} {
, m_nPreWords{nPreWords}
, m_senItem2TrigIdx{std::move(senItem2TrigIdx)} {
// If no triggers, we don't need to generate anything
if (!m_nVecWords) return;
// Othewise construc the parts of the kit
@ -437,6 +453,7 @@ TriggerKit::TriggerKit(const std::string& name, bool slow, uint32_t nSenseWords,
AstRange* const ep = new AstRange{flp, static_cast<int>(m_nVecWords + m_nPreWords - 1), 0};
m_trigExtDTypep = new AstUnpackArrayDType{flp, m_wordDTypep, ep};
netlistp->typeTablep()->addTypesp(m_trigExtDTypep);
m_compExtp = util::makeSubFunction(netlistp, "_eval_triggers_ext__" + m_name, m_slow);
} else {
m_trigExtDTypep = m_trigVecDTypep;
}
@ -444,11 +461,40 @@ TriggerKit::TriggerKit(const std::string& name, bool slow, uint32_t nSenseWords,
m_vscp = scopep->createTemp("__V" + m_name + "Triggered", m_trigExtDTypep);
m_vscp->varp()->isInternal(true);
// The trigger computation function
m_compp = util::makeSubFunction(netlistp, "_eval_triggers__" + m_name, m_slow);
m_compVecp = util::makeSubFunction(netlistp, "_eval_triggers_vec__" + m_name, m_slow);
// The debug dump function, always 'slow'
m_dumpp = util::makeSubFunction(netlistp, "_dump_triggers__" + m_name, true);
m_dumpp->isStatic(true);
m_dumpp->ifdef("VL_DEBUG");
if (useAcc) {
m_vscAccp = scopep->createTemp("__V" + m_name + "TriggeredAcc", m_trigVecDTypep);
m_vscAccp->varp()->isInternal(true);
}
}
AstAssign* TriggerKit::createSenTrigVecAssignment(AstVarScope* const target,
std::vector<AstNodeExpr*>& trigps) {
FileLine* const flp = target->fileline();
AstAssign* trigStmtsp = nullptr;
// Assign sense triggers vector one word at a time
for (size_t i = 0; i < trigps.size(); i += WORD_SIZE) {
// Concatenate all bits in this trigger word using a balanced
for (uint32_t level = 0; level < WORD_SIZE_LOG2; ++level) {
const uint32_t stride = 1 << level;
for (uint32_t j = 0; j < WORD_SIZE; j += 2 * stride) {
trigps[i + j] = new AstConcat{trigps[i + j]->fileline(), trigps[i + j + stride],
trigps[i + j]};
trigps[i + j + stride] = nullptr;
}
}
// Set the whole word in the trigger vector
const int wordIndex = static_cast<int>(i / WORD_SIZE);
AstArraySel* const aselp
= new AstArraySel{flp, new AstVarRef{flp, target, VAccess::WRITE}, wordIndex};
trigStmtsp = AstNode::addNext(trigStmtsp, new AstAssign{flp, aselp, trigps[i]});
}
return trigStmtsp;
}
TriggerKit TriggerKit::create(AstNetlist* netlistp, //
@ -458,7 +504,8 @@ TriggerKit TriggerKit::create(AstNetlist* netlistp, //
const std::vector<const AstSenTree*>& senTreeps, //
const string& name, //
const ExtraTriggers& extraTriggers, //
bool slow) {
bool slow, //
bool useAcc) {
// Need to gather all the unique SenItems under the given SenTrees
// List of unique SenItems used by all 'senTreeps'
@ -512,7 +559,7 @@ TriggerKit TriggerKit::create(AstNetlist* netlistp, //
const uint32_t nExtraWords = nExtraTriggers / WORD_SIZE;
// We can now construct the trigger kit - this constructs all items that will be kept
TriggerKit kit{name, slow, nSenseWords, nExtraWords, nPreWords};
TriggerKit kit{name, slow, nSenseWords, nExtraWords, nPreWords, senItem2TrigIdx, useAcc};
// If there are no triggers we are done
if (!kit.m_nVecWords) return kit;
@ -587,7 +634,11 @@ TriggerKit TriggerKit::create(AstNetlist* netlistp, //
AstNodeExpr* const wordp = new AstArraySel{flp, wr(kit.m_vscp), wrdIndex};
AstNodeExpr* const lhsp = new AstSel{flp, wordp, bitIndex, 1};
AstNodeExpr* const rhsp = new AstConst{flp, AstConst::BitTrue{}};
initialTrigsp = AstNode::addNext(initialTrigsp, new AstAssign{flp, lhsp, rhsp});
if (useAcc) {
initFuncp->addStmtsp(new AstAssign{flp, lhsp, rhsp});
} else {
initialTrigsp = AstNode::addNext(initialTrigsp, new AstAssign{flp, lhsp, rhsp});
}
}
// Add a debug statement for this trigger
@ -601,25 +652,7 @@ TriggerKit TriggerKit::create(AstNetlist* netlistp, //
}
UASSERT(trigps.size() == nSenseTriggers, "Inconsistent number of trigger expressions");
// Assign sense triggers vector one word at a time
AstNodeStmt* trigStmtsp = nullptr;
for (size_t i = 0; i < nSenseTriggers; i += WORD_SIZE) {
// Concatenate all bits in this trigger word using a balanced
for (uint32_t level = 0; level < WORD_SIZE_LOG2; ++level) {
const uint32_t stride = 1 << level;
for (uint32_t j = 0; j < WORD_SIZE; j += 2 * stride) {
trigps[i + j] = new AstConcat{trigps[i + j]->fileline(), trigps[i + j + stride],
trigps[i + j]};
trigps[i + j + stride] = nullptr;
}
}
// Set the whole word in the trigger vector
const int wordIndex = static_cast<int>(i / WORD_SIZE);
AstArraySel* const aselp = new AstArraySel{flp, wr(kit.m_vscp), wordIndex};
trigStmtsp = AstNode::addNext(trigStmtsp, new AstAssign{flp, aselp, trigps[i]});
}
trigps.clear();
AstAssign* const trigStmtsp = createSenTrigVecAssignment(kit.m_vscp, trigps);
// Add a print for each of the extra triggers
for (unsigned i = 0; i < extraTriggers.size(); ++i) {
@ -652,18 +685,18 @@ TriggerKit TriggerKit::create(AstNetlist* netlistp, //
}
// Get the SenExprBuilder results
const SenExprBuilder::Results senResults = senExprBuilder.getAndClearResults();
const SenExprBuilder::Results senResults = senExprBuilder.getResultsAndClearUpdates();
// Add the SenExprBuilder init statements to the static initialization functino
for (AstNodeStmt* const nodep : senResults.m_inits) initFuncp->addStmtsp(nodep);
// Assemble the trigger computation function
// Assemble the base trigger computation function
AstScope* const scopep = netlistp->topScopep()->scopep();
{
AstCFunc* const fp = kit.m_compp;
AstScope* const scopep = netlistp->topScopep()->scopep();
AstCFunc* const fp = kit.m_compVecp;
// Profiling push
if (v3Global.opt.profExec()) {
fp->addStmtsp(AstCStmt::profExecSectionPush(flp, "trig " + name));
fp->addStmtsp(AstCStmt::profExecSectionPush(flp, "trigBase " + name));
}
// Trigger computation
for (AstNodeStmt* const nodep : senResults.m_preUpdates) fp->addStmtsp(nodep);
@ -678,39 +711,39 @@ TriggerKit TriggerKit::create(AstNetlist* netlistp, //
ifp->addThensp(util::setVar(initVscp, 1));
ifp->addThensp(initialTrigsp);
}
// If there are 'pre' triggers, compute them
if (kit.m_nPreWords) {
// Add an argument to the function that takes the latched values
AstVarScope* const latchedp
= newArgument(fp, kit.m_trigVecDTypep, "latched", VDirection::CONSTREF);
// Add loop counter variable - this can't be local because we call util::splitCheck
AstVarScope* const nVscp = scopep->createTemp("__V" + name + "TrigPreLoopCounter", 32);
nVscp->varp()->noReset(true);
// Add a loop to compute the pre words
AstLoop* const loopp = new AstLoop{flp};
fp->addStmtsp(util::setVar(nVscp, 0));
fp->addStmtsp(loopp);
// Loop body
AstNodeExpr* const offsetp = new AstConst{flp, kit.m_nVecWords};
AstNodeExpr* const lIdxp = new AstAdd{flp, rd(nVscp), offsetp};
AstNodeExpr* const lhsp = new AstArraySel{flp, wr(kit.m_vscp), lIdxp};
AstNodeExpr* const aWordp = new AstArraySel{flp, rd(kit.m_vscp), rd(nVscp)};
AstNodeExpr* const bWordp = new AstArraySel{flp, rd(latchedp), rd(nVscp)};
AstNodeExpr* const rhsp = new AstAnd{flp, aWordp, new AstNot{flp, bWordp}};
AstNodeExpr* const limp = new AstConst{flp, AstConst::WidthedValue{}, 32, nPreWords};
loopp->addStmtsp(new AstAssign{flp, lhsp, rhsp});
loopp->addStmtsp(util::incrementVar(nVscp));
loopp->addStmtsp(new AstLoopTest{flp, loopp, new AstLt{flp, rd(nVscp), limp}});
}
// Add a call to the dumping function if debug is enabled
fp->addStmtsp(kit.newDumpCall(kit.m_vscp, name, true));
// Profiling pop
if (v3Global.opt.profExec()) {
fp->addStmtsp(AstCStmt::profExecSectionPop(flp, "trig " + name));
fp->addStmtsp(AstCStmt::profExecSectionPop(flp, "trigBase " + name));
}
// Done with the trigger computation function, split as might be large
util::splitCheck(fp);
};
// If there are 'pre' triggers, compute them
if (kit.m_nPreWords) {
AstCFunc* const fp = kit.m_compExtp;
// Add an argument to the function that takes the latched values
AstVarScope* const latchedp
= newArgument(fp, kit.m_trigVecDTypep, "latched", VDirection::CONSTREF);
// Add loop counter variable - this can't be local because we call util::splitCheck
AstVarScope* const nVscp = scopep->createTemp("__V" + name + "TrigPreLoopCounter", 32);
nVscp->varp()->noReset(true);
// Add a loop to compute the pre words
AstLoop* const loopp = new AstLoop{flp};
fp->addStmtsp(util::setVar(nVscp, 0));
fp->addStmtsp(loopp);
// Loop body
AstNodeExpr* const offsetp = new AstConst{flp, kit.m_nVecWords};
AstNodeExpr* const lIdxp = new AstAdd{flp, rd(nVscp), offsetp};
AstNodeExpr* const lhsp = new AstArraySel{flp, wr(kit.m_vscp), lIdxp};
AstNodeExpr* const aWordp = new AstArraySel{flp, rd(kit.m_vscp), rd(nVscp)};
AstNodeExpr* const bWordp = new AstArraySel{flp, rd(latchedp), rd(nVscp)};
AstNodeExpr* const rhsp = new AstAnd{flp, aWordp, new AstNot{flp, bWordp}};
AstNodeExpr* const limp = new AstConst{flp, AstConst::WidthedValue{}, 32, nPreWords};
loopp->addStmtsp(new AstAssign{flp, lhsp, rhsp});
loopp->addStmtsp(util::incrementVar(nVscp));
loopp->addStmtsp(new AstLoopTest{flp, loopp, new AstLt{flp, rd(nVscp), limp}});
util::splitCheck(fp);
}
// Done with the trigger computation function, split as might be large
// The debug code might leak signal names, so simply delete it when using --protect-ids
if (v3Global.opt.protectIds()) kit.m_dumpp->stmtsp()->unlinkFrBackWithNext()->deleteTree();
@ -720,4 +753,280 @@ TriggerKit TriggerKit::create(AstNetlist* netlistp, //
return kit;
}
// Find all CAwaits, clear SenTrees inside them, generate before-trigger functions (functions that
// shall be called before awaiting for a VCMethod::SCHED_TRIGGER) and add thier calls before
// proper CAwaits
class AwaitBeforeTrigVisitor final : public VNVisitor {
const VNUser1InUse m_user1InUse;
/**
* AstCAwait::user1() -> bool. True if node has been visited
* AstSenTree::user1p() -> AstCFunc*. Function that has to be called before awaiting
* for CAwait pointing to this SenTree
* AstCFunc::user1p() -> AstVarScope* Function's local temporary extended trigger
* vector variable scope
*/
// Netlist - needed for using util::makeSubFunction()
AstNetlist* const m_netlistp;
// Trigger kit - for accessing trigger vectors and mapping senItems to thier indexes
const TriggerKit& m_trigKit;
// Expression builder - for building expressions from SenItems
SenExprBuilder& m_senExprBuilder;
// Generator of unique names for before-trigger function
V3UniqueNames m_beforeTriggerFuncUniqueName;
// Vector containing every generated CFuncs and related SenTree
std::vector<std::pair<AstCFunc*, AstSenTree*>> m_generatedFuncs;
// Map from SenTree to coresponding scheduler
std::map<AstSenTree*, AstNodeExpr*> m_senTreeToSched;
// Map containing vectors of SenItems that share the same prevValue variable
std::unordered_map<VNRef<AstNode>, std::vector<AstSenItem*>> m_senExprToSenItem;
// Returns node which is used for grouping SenItems in `m_senExprToSenItem`
static AstNode* getSenHashNode(const AstSenItem* const nodep) {
if (AstVarRef* const varRefp = VN_CAST(nodep->sensp(), VarRef)) return varRefp;
return nodep->sensp();
}
// Populates `m_senExprToSenItem` with every group of SenItems that share the same prevValue
// variable. Groups that contain only one type of an edge are omitted.
void fillSenExprToSenItem() {
for (auto senTreeSched : m_senTreeToSched) {
AstSenTree* const senTreep = senTreeSched.first;
for (AstSenItem* senItemp = senTreep->sensesp(); senItemp;
senItemp = VN_AS(senItemp->nextp(), SenItem)) {
const VEdgeType edge = senItemp->edgeType();
if (edge.anEdge() || edge == VEdgeType::ET_CHANGED
|| edge == VEdgeType::ET_HYBRID) {
m_senExprToSenItem[*getSenHashNode(senItemp)].push_back(senItemp);
}
}
}
std::vector<VNRef<AstNode>> toRemove;
for (const auto& senExprToSenTree : m_senExprToSenItem) {
std::vector<AstSenItem*> senItemps = senExprToSenTree.second;
toRemove.push_back(senExprToSenTree.first);
for (size_t i = 1; i < senItemps.size(); ++i) {
if (senItemps[i]->edgeType() != senItemps[i - 1]->edgeType()) {
toRemove.pop_back();
break;
}
}
}
for (VNRef<AstNode> it : toRemove) m_senExprToSenItem.erase(it);
}
// For set of bits indexes (of sensitivity vector) return map from those indexes to set
// of schedulers sensitive to these indexes. Indices are split into word index and bit
// masking this index within given word
std::map<size_t, std::map<size_t, std::set<AstNodeExpr*>>>
getUsedTriggersToTrees(const std::set<size_t>& usedTriggers) {
std::map<size_t, std::map<size_t, std::set<AstNodeExpr*>>> usedTrigsToUsingTrees;
for (auto senTreeSched : m_senTreeToSched) {
const AstSenTree* const senTreep = senTreeSched.first;
AstNodeExpr* const shedp = senTreeSched.second;
// Find all common SenItem indexes for `senTreep` and `usedTriggers`
std::set<size_t> usedTriggersInSenTree;
for (AstSenItem* senItemp = senTreep->sensesp(); senItemp;
senItemp = VN_AS(senItemp->nextp(), SenItem)) {
const size_t idx = m_trigKit.senItem2TrigIdx(senItemp);
if (usedTriggers.find(idx) != usedTriggers.end()) {
usedTrigsToUsingTrees[idx / TriggerKit::WORD_SIZE]
[1 << (idx % TriggerKit::WORD_SIZE)]
.insert(shedp);
}
}
}
return usedTrigsToUsingTrees;
}
// Returns a CCall to a before-trigger function for a given SenTree,
// Constructs such a function if it doesn't exist yet
AstCCall* getBeforeTriggerStmt(AstSenTree* const senTreep) {
FileLine* const flp = senTreep->fileline();
if (!senTreep->user1p()) {
AstCFunc* const funcp = util::makeSubFunction(
m_netlistp, m_beforeTriggerFuncUniqueName.get(senTreep), false);
senTreep->user1p(funcp);
// Create a local temporary extended vector
AstVarScope* const vscAccp = m_trigKit.vscAccp();
AstVarScope* const tmpp = vscAccp->scopep()->createTempLike("__VTmp", vscAccp);
AstVar* const tmpVarp = tmpp->varp()->unlinkFrBack();
funcp->user1p(tmpp);
funcp->addVarsp(tmpVarp);
tmpVarp->funcLocal(true);
tmpVarp->noReset(true);
AstVar* const argp = new AstVar{flp, VVarType::BLOCKTEMP, "__VeventDescription",
senTreep->findBasicDType(VBasicDTypeKwd::CHARPTR)};
argp->funcLocal(true);
argp->direction(VDirection::INPUT);
funcp->addArgsp(argp);
// Scope is created in the constructor after iterate finishes
m_generatedFuncs.emplace_back(funcp, senTreep);
}
AstCCall* const callp = new AstCCall{flp, VN_AS(senTreep->user1p(), CFunc)};
callp->dtypeSetVoid();
return callp;
}
void visit(AstCAwait* const nodep) override {
if (nodep->user1SetOnce()) return;
// Check whether it is a CAwait for a VCMethod::SCHED_TRIGGER
if (const AstCMethodHard* const cMethodHardp = VN_CAST(nodep->exprp(), CMethodHard)) {
if (cMethodHardp->method() == VCMethod::SCHED_TRIGGER) {
AstCCall* const beforeTrigp = getBeforeTriggerStmt(nodep->sentreep());
FileLine* const flp = nodep->fileline();
// Add eventDescription argument value to a CCall - it is used for --runtime-debug
AstNode* const pinp = cMethodHardp->pinsp()->nextp()->nextp();
UASSERT_OBJ(pinp, cMethodHardp, "No event description");
beforeTrigp->addArgsp(VN_AS(pinp, NodeExpr)->cloneTree(false));
// Change CAwait Expression into StmtExpr that calls to a before-trigger function
// first and then return CAwait
VNRelinker relinker;
nodep->unlinkFrBack(&relinker);
AstExprStmt* const exprstmtp
= new AstExprStmt{flp, beforeTrigp->makeStmt(), nodep};
relinker.relink(exprstmtp);
m_senTreeToSched.emplace(nodep->sentreep(), cMethodHardp->fromp());
}
}
nodep->clearSentreep(); // Clear as these sentrees will get deleted later
iterate(nodep);
}
void visit(AstNode* const nodep) override { iterateChildren(nodep); }
public:
AwaitBeforeTrigVisitor(AstNetlist* netlistp, SenExprBuilder& senExprBuilder,
const TriggerKit& trigKit)
: m_netlistp{netlistp}
, m_trigKit{trigKit}
, m_senExprBuilder{senExprBuilder}
, m_beforeTriggerFuncUniqueName{"__VbeforeTrig"} {
iterate(netlistp);
fillSenExprToSenItem();
std::vector<AstNodeExpr*> trigps;
std::set<size_t> usedTriggers;
// In each of before-trigger functions check if anything was triggered and mark as ready
// triggered schedulers
for (const auto& funcToUsedTriggers : m_generatedFuncs) {
AstCFunc* const funcp = funcToUsedTriggers.first;
AstVarScope* const vscp = VN_AS(funcp->user1p(), VarScope);
FileLine* const flp = funcp->fileline();
// Generate trigger evaluation
{
AstSenTree* const senTreep = funcToUsedTriggers.second;
// Puts `exprp` at `pos` and makes sure that trigps.size() is multiple of
// TriggerKit::WORD_SIZE
const auto emplaceAt
= [flp, &trigps, &usedTriggers](AstNodeExpr* const exprp, const size_t pos) {
const size_t targetSize
= vlstd::roundUpToMultipleOf<TriggerKit::WORD_SIZE>(pos + 1);
if (trigps.capacity() < targetSize) trigps.reserve(targetSize * 2);
while (trigps.size() < targetSize) {
trigps.push_back(new AstConst{flp, AstConst::BitFalse{}});
}
trigps[pos]->deleteTree();
trigps[pos] = exprp;
usedTriggers.insert(pos);
};
// Find all trigger indexes of SenItems inside `senTreep`
// and add them to `trigps` and `usedTriggers`
for (const AstSenItem* itemp = senTreep->sensesp(); itemp;
itemp = VN_AS(itemp->nextp(), SenItem)) {
const size_t idx = m_trigKit.senItem2TrigIdx(itemp);
emplaceAt(m_senExprBuilder.build(itemp).first, idx);
auto iter = m_senExprToSenItem.find(*getSenHashNode(itemp));
if (iter != m_senExprToSenItem.end()) {
for (AstSenItem* const additionalItemp : iter->second) {
const size_t idx = m_trigKit.senItem2TrigIdx(additionalItemp);
emplaceAt(m_senExprBuilder.build(additionalItemp).first, idx);
}
}
}
// Fill the function with neccessary statements
SenExprBuilder::Results results = m_senExprBuilder.getResultsAndClearUpdates();
for (AstNodeStmt* const stmtsp : results.m_inits) funcp->addStmtsp(stmtsp);
for (AstNodeStmt* const stmtsp : results.m_preUpdates) funcp->addStmtsp(stmtsp);
funcp->addStmtsp(TriggerKit::createSenTrigVecAssignment(vscp, trigps));
trigps.clear();
for (AstNodeStmt* const stmtsp : results.m_postUpdates) funcp->addStmtsp(stmtsp);
}
std::map<size_t, std::map<size_t, std::set<AstNodeExpr*>>> usedTrigsToUsingTrees
= getUsedTriggersToTrees(usedTriggers);
usedTriggers.clear();
// Helper returning expression getting array index `idx` from `scocep` with access
// `access`
const auto getIdx = [flp](AstVarScope* const scocep, VAccess access, size_t idx) {
return new AstArraySel{flp, new AstVarRef{flp, scocep, access},
new AstConst{flp, AstConst::Unsized64{}, idx}};
};
// Get eventDescription argument
AstVarScope* const argpVscp = new AstVarScope{flp, funcp->scopep(), funcp->argsp()};
funcp->scopep()->addVarsp(argpVscp);
// Mark as ready triggered schedulers
for (const auto& triggersToTrees : usedTrigsToUsingTrees) {
const size_t word = triggersToTrees.first;
for (const auto& bitsToTrees : triggersToTrees.second) {
const size_t bit = bitsToTrees.first;
const auto& schedulers = bitsToTrees.second;
// Check if given bit is fired - single bits are checked since
// usually there is only a few of them (only one most of the times as we await
// only for one event)
AstConst* const maskConstp = new AstConst{flp, AstConst::Unsized64{}, bit};
AstAnd* const condp
= new AstAnd{flp, getIdx(vscp, VAccess::READ, word), maskConstp};
AstIf* const ifp = new AstIf{flp, condp};
// Call ready() on each scheduler sensitive to `condp`
for (AstNodeExpr* const schedp : schedulers) {
AstCMethodHard* const callp = new AstCMethodHard{
flp, schedp->cloneTree(false), VCMethod::SCHED_READY};
callp->dtypeSetVoid();
callp->addPinsp(new AstVarRef{flp, argpVscp, VAccess::READ});
ifp->addThensp(callp->makeStmt());
}
funcp->addStmtsp(ifp);
}
}
AstVarScope* const vscAccp = m_trigKit.vscAccp();
// Add touched values to accumulator
for (const auto& triggersToTrees : usedTrigsToUsingTrees) {
const size_t word = triggersToTrees.first;
funcp->addStmtsp(new AstAssign{flp, getIdx(vscAccp, VAccess::WRITE, word),
new AstOr{flp, getIdx(vscAccp, VAccess::READ, word),
getIdx(vscp, VAccess::READ, word)}});
}
}
}
~AwaitBeforeTrigVisitor() override = default;
};
void beforeTrigVisitor(AstNetlist* netlistp, SenExprBuilder& senExprBuilder,
const TriggerKit& trigKit) {
AwaitBeforeTrigVisitor{netlistp, senExprBuilder, trigKit};
}
} // namespace V3Sched

View File

@ -322,6 +322,14 @@ public:
return {resultp, firedAtInitialization};
}
Results getResultsAndClearUpdates() {
m_hasPreUpdate.clear();
m_hasPostUpdate.clear();
Results ans = std::move(m_results);
m_results = {};
return ans;
}
Results getAndClearResults() {
m_curr.clear();
m_prev.clear();

View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# Copyright 2026 by Wilson Snyder. This program is free software; you can
# redistribute it and/or modify it under the terms of either the GNU
# Lesser General Public License Version 3 or the Perl Artistic License
# Version 2.0.
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('simulator')
test.compile(verilator_flags2=["--binary"])
test.execute()
test.passes()

View File

@ -0,0 +1,33 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain, for
// any use, without warranty, 2026 by Antmicro.
// SPDX-License-Identifier: CC0-1.0
module x;
event a;
int counter = 0;
initial begin
fork
begin
@a;
->a;
@a;
counter++;
end
join_none
#1;
->a;
end
always begin
@a;
->a;
@a;
counter++;
end
final begin
if (counter != 1) $stop;
$write("*-* All Finished *-*\n");
end
endmodule

View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# Copyright 2026 by Wilson Snyder. This program is free software; you can
# redistribute it and/or modify it under the terms of either the GNU
# Lesser General Public License Version 3 or the Perl Artistic License
# Version 2.0.
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('simulator')
test.compile(verilator_flags2=["--binary"])
test.execute()
test.passes()

View File

@ -0,0 +1,34 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain, for
// any use, without warranty, 2026 by Antmicro.
// SPDX-License-Identifier: CC0-1.0
module x;
event a;
event b;
bit ok = 0;
task do_the_job();
static bit first = 1;
if (first) begin
first = 0;
@a;
ok = 1;
end
else begin
->a;
end
endtask
initial begin
#1 ->b;
#100;
if (!ok) $stop;
$write("*-* All Finished *-*\n");
$finish;
end
always @b do_the_job();
always @b do_the_job();
endmodule

View File

@ -125,7 +125,7 @@ test.file_grep_not(test.obj_dir + "/" + test.vm_prefix + "_classes.mk", "vm_clas
test.file_grep_not(test.obj_dir + "/" + test.vm_prefix + "_classes.mk", "vm_classes_2")
# Check combine count
test.file_grep(test.stats, r'Node count, CFILE + (\d+)', (279 if test.vltmt else 262))
test.file_grep(test.stats, r'Node count, CFILE + (\d+)', (276 if test.vltmt else 259))
test.file_grep(test.stats, r'Makefile targets, VM_CLASSES_FAST + (\d+)', 2)
test.file_grep(test.stats, r'Makefile targets, VM_CLASSES_SLOW + (\d+)', 2)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of either the GNU Lesser General Public License Version 3
# or the Perl Artistic License Version 2.0.
# SPDX-FileCopyrightText: 2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('simulator')
test.compile(verilator_flags2=["--binary"])
test.execute()
test.passes()

View File

@ -0,0 +1,25 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain, for
// any use, without warranty, 2026 by Antmicro.
// SPDX-License-Identifier: CC0-1.0
module x;
bit val = 0;
bit ok = 0;
initial #1 begin
val = 1;
@(val);
$write("*-* All Finished *-*\n");
$finish;
end
initial @(posedge val) begin
val = 0;
ok = 1;
@(edge val);
$stop;
end
initial #10 $stop;
endmodule

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,181 @@
-V{t#,#}- Verilated::debug is on. Message prefix indicates {<thread>,<sequence_number>}.
-V{t#,#}+ Vt_timing_eval_act___024root___ctor_var_reset
-V{t#,#}+++++TOP Evaluate Vt_timing_eval_act::eval_step
-V{t#,#}+ Vt_timing_eval_act___024root___eval_debug_assertions
-V{t#,#}+ Initial
-V{t#,#}+ Vt_timing_eval_act___024root___eval_static
-V{t#,#}+ Vt_timing_eval_act___024root___timing_ready
-V{t#,#}+ Vt_timing_eval_act___024root___eval_initial
-V{t#,#}+ Vt_timing_eval_act___024root___eval_initial__TOP__Vtiming__0
-V{t#,#}+ Vt_timing_eval_act___024root____VbeforeTrig_h########__0
-V{t#,#} Suspending process waiting for @([event] t.a) at t/t_timing_eval_act.v:18
-V{t#,#}+ Vt_timing_eval_act___024root___eval_initial__TOP__Vtiming__1
-V{t#,#}+ Vt_timing_eval_act___024root____VbeforeTrig_h########__0
-V{t#,#} Suspending process waiting for @(posedge t.clk_inv) at t/t_timing_eval_act.v:25
-V{t#,#}+ Vt_timing_eval_act___024root___eval_initial__TOP__Vtiming__2
-V{t#,#}+ Vt_timing_eval_act___024root____VbeforeTrig_h########__0
-V{t#,#} Suspending process waiting for @([event] t.a) at t/t_timing_eval_act.v:33
-V{t#,#}+ Vt_timing_eval_act___024root___eval_initial__TOP__Vtiming__3
-V{t#,#}+ Vt_timing_eval_act___024root___eval_settle
-V{t#,#}+ Vt_timing_eval_act___024root___eval_phase__stl
-V{t#,#}+ Vt_timing_eval_act___024root___eval_triggers_vec__stl
-V{t#,#}+ Vt_timing_eval_act___024root___dump_triggers__stl
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__stl
-V{t#,#} 'stl' region trigger index 0 is active: Internal 'stl' trigger - first iteration
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__stl
-V{t#,#}+ Vt_timing_eval_act___024root___eval_stl
-V{t#,#}+ Vt_timing_eval_act___024root___act_comb__TOP__0
-V{t#,#}+ Vt_timing_eval_act___024root___eval_phase__stl
-V{t#,#}+ Vt_timing_eval_act___024root___eval_triggers_vec__stl
-V{t#,#}+ Vt_timing_eval_act___024root___dump_triggers__stl
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__stl
-V{t#,#} No 'stl' region triggers active
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__stl
-V{t#,#}+ Eval
-V{t#,#}+ Vt_timing_eval_act___024root___eval
-V{t#,#}+ Vt_timing_eval_act___024root___eval_phase__act
-V{t#,#}+ Vt_timing_eval_act___024root___eval_triggers_vec__act
-V{t#,#}+ Vt_timing_eval_act___024root___timing_ready
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_orInto__act_vec_vec
-V{t#,#}+ Vt_timing_eval_act___024root___dump_triggers__act
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__act
-V{t#,#} No 'act' region triggers active
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_orInto__act_vec_vec
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__act
-V{t#,#}+ Vt_timing_eval_act___024root___eval_phase__nba
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__act
-V{t#,#}End-of-eval cleanup
-V{t#,#}+++++TOP Evaluate Vt_timing_eval_act::eval_step
-V{t#,#}+ Vt_timing_eval_act___024root___eval_debug_assertions
-V{t#,#}+ Eval
-V{t#,#}+ Vt_timing_eval_act___024root___eval
-V{t#,#}+ Vt_timing_eval_act___024root___eval_phase__act
-V{t#,#}+ Vt_timing_eval_act___024root___eval_triggers_vec__act
-V{t#,#}+ Vt_timing_eval_act___024root___timing_ready
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_orInto__act_vec_vec
-V{t#,#}+ Vt_timing_eval_act___024root___dump_triggers__act
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__act
-V{t#,#} 'act' region trigger index 3 is active: @([true] __VdlySched.awaitingCurrentTime())
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_orInto__act_vec_vec
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__act
-V{t#,#}+ Vt_timing_eval_act___024root___timing_resume
-V{t#,#} No process to resume waiting for @([event] t.a)
-V{t#,#} Not triggered processes waiting for @([event] t.a):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:18
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:33
-V{t#,#} Resuming processes waiting for @([event] t.a)
-V{t#,#} No process to resume waiting for @(posedge t.clk_inv)
-V{t#,#} Not triggered processes waiting for @(posedge t.clk_inv):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:25
-V{t#,#} Resuming processes waiting for @(posedge t.clk_inv)
-V{t#,#} No process to resume waiting for @([event] t.e)
-V{t#,#} Resuming processes waiting for @([event] t.e)
-V{t#,#} Delayed processes:
-V{t#,#} Awaiting time 1: Process waiting at t/t_timing_eval_act.v:39
-V{t#,#} Resuming delayed processes
-V{t#,#} Resuming: Process waiting at t/t_timing_eval_act.v:39
-V{t#,#}+ Vt_timing_eval_act___024root___eval_act
-V{t#,#}+ Vt_timing_eval_act___024root___act_comb__TOP__0
-V{t#,#}+ Vt_timing_eval_act___024root___eval_phase__act
-V{t#,#}+ Vt_timing_eval_act___024root___eval_triggers_vec__act
-V{t#,#}+ Vt_timing_eval_act___024root___timing_ready
-V{t#,#} Committing processes waiting for @([event] t.a):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:18
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:33
-V{t#,#} Committing processes waiting for @(posedge t.clk_inv):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:25
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_orInto__act_vec_vec
-V{t#,#}+ Vt_timing_eval_act___024root___dump_triggers__act
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__act
-V{t#,#} 'act' region trigger index 0 is active: @([event] t.a)
-V{t#,#} 'act' region trigger index 1 is active: @(posedge t.clk_inv)
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_orInto__act_vec_vec
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__act
-V{t#,#}+ Vt_timing_eval_act___024root___timing_resume
-V{t#,#} Moving to resume queue processes waiting for @([event] t.a):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:18
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:33
-V{t#,#} Moving to resume queue processes waiting for @(posedge t.clk_inv):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:25
-V{t#,#} Processes to resume waiting for @([event] t.a):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:18
-V{t#,#} Processes to resume waiting for @([event] t.a):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:33
-V{t#,#} Resuming processes waiting for @([event] t.a)
-V{t#,#} Resuming: Process waiting at t/t_timing_eval_act.v:18
-V{t#,#}+ Vt_timing_eval_act___024root____VbeforeTrig_h########__0
-V{t#,#} Suspending process waiting for @(posedge t.clk_inv) at t/t_timing_eval_act.v:19
-V{t#,#} Resuming: Process waiting at t/t_timing_eval_act.v:33
-V{t#,#}+ Vt_timing_eval_act___024root____VbeforeTrig_h########__0
-V{t#,#} Suspending process waiting for @([event] t.e) at t/t_timing_eval_act.v:34
-V{t#,#} Processes to resume waiting for @(posedge t.clk_inv):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:25
-V{t#,#} Not triggered processes waiting for @(posedge t.clk_inv):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:19
-V{t#,#} Resuming processes waiting for @(posedge t.clk_inv)
-V{t#,#} Resuming: Process waiting at t/t_timing_eval_act.v:25
-V{t#,#}+ Vt_timing_eval_act___024root____VbeforeTrig_h########__0
-V{t#,#} Committing processes waiting for @([event] t.e):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:34
-V{t#,#} Suspending process waiting for @([event] t.e) at t/t_timing_eval_act.v:28
-V{t#,#} No process to resume waiting for @([event] t.e)
-V{t#,#} Triggered processes waiting for @([event] t.e):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:28
-V{t#,#} Not triggered processes waiting for @([event] t.e):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:28
-V{t#,#} Resuming processes waiting for @([event] t.e)
-V{t#,#}+ Vt_timing_eval_act___024root___eval_act
-V{t#,#}+ Vt_timing_eval_act___024root___act_comb__TOP__0
-V{t#,#}+ Vt_timing_eval_act___024root___eval_phase__act
-V{t#,#}+ Vt_timing_eval_act___024root___eval_triggers_vec__act
-V{t#,#}+ Vt_timing_eval_act___024root___timing_ready
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_orInto__act_vec_vec
-V{t#,#}+ Vt_timing_eval_act___024root___dump_triggers__act
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__act
-V{t#,#} 'act' region trigger index 2 is active: @([event] t.e)
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_orInto__act_vec_vec
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__act
-V{t#,#}+ Vt_timing_eval_act___024root___timing_resume
-V{t#,#} Moving to resume queue processes waiting for @([event] t.e):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:34
-V{t#,#} No process to resume waiting for @([event] t.a)
-V{t#,#} Resuming processes waiting for @([event] t.a)
-V{t#,#} No process to resume waiting for @(posedge t.clk_inv)
-V{t#,#} Not triggered processes waiting for @(posedge t.clk_inv):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:19
-V{t#,#} Resuming processes waiting for @(posedge t.clk_inv)
-V{t#,#} Processes to resume waiting for @([event] t.e):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:34
-V{t#,#} Not triggered processes waiting for @([event] t.e):
-V{t#,#} - Process waiting at t/t_timing_eval_act.v:28
-V{t#,#} Resuming processes waiting for @([event] t.e)
-V{t#,#} Resuming: Process waiting at t/t_timing_eval_act.v:34
-V{t#,#}+ Vt_timing_eval_act___024root___eval_act
-V{t#,#}+ Vt_timing_eval_act___024root___act_comb__TOP__0
-V{t#,#}+ Vt_timing_eval_act___024root___eval_phase__act
-V{t#,#}+ Vt_timing_eval_act___024root___eval_triggers_vec__act
-V{t#,#}+ Vt_timing_eval_act___024root___timing_ready
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_orInto__act_vec_vec
-V{t#,#}+ Vt_timing_eval_act___024root___dump_triggers__act
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__act
-V{t#,#} No 'act' region triggers active
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_orInto__act_vec_vec
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__act
-V{t#,#}+ Vt_timing_eval_act___024root___eval_phase__nba
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__act
-V{t#,#}+ Vt_timing_eval_act___024root___eval_nba
-V{t#,#}+ Vt_timing_eval_act___024root___act_comb__TOP__0
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_clear__act
-V{t#,#}+ Vt_timing_eval_act___024root___eval_phase__act
-V{t#,#}+ Vt_timing_eval_act___024root___eval_triggers_vec__act
-V{t#,#}+ Vt_timing_eval_act___024root___timing_ready
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_orInto__act_vec_vec
-V{t#,#}+ Vt_timing_eval_act___024root___dump_triggers__act
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__act
-V{t#,#} No 'act' region triggers active
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_orInto__act_vec_vec
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__act
-V{t#,#}+ Vt_timing_eval_act___024root___eval_phase__nba
-V{t#,#}+ Vt_timing_eval_act___024root___trigger_anySet__act
-V{t#,#}End-of-eval cleanup
-V{t#,#}+ Vt_timing_eval_act___024root___eval_final

View File

@ -0,0 +1,28 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# Copyright 2026 by Wilson Snyder. This program is free software; you
# can redistribute it and/or modify it under the terms of either the GNU
# Lesser General Public License Version 3 or the Perl Artistic License
# Version 2.0.
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('vlt')
test.compile(verilator_flags2=["--binary", "--runtime-debug"])
test.file_grep(
test.obj_dir + "/" + test.vm_prefix + "___024root__0.cpp", r'void\s+' + test.vm_prefix +
r'___024root___timing_resume\(' + test.vm_prefix + r'___024root\*\s+vlSelf\)\s+\{' +
r'\n((?!})(.|\n))*' + r'\/\*' + test.top_filename + r':18\s0x[0-9a-f]+\*\/\s*' +
r'vlSelfRef\.__VtrigSched_[\d\w]*\.resume\([\n\s]*\"@\(\[event\]\st\.a\)\"\);\n\s+' + r'\/\*' +
test.top_filename + r':19\s0x[0-9a-f]+\*\/\s*' +
r'vlSelfRef\.__VtrigSched_[\d\w]*\.resume\([\n\s]*\"@\(posedge\st\.clk_inv\)\"\);\n\s+' +
r'\/\*' + test.top_filename + r':20\s0x[0-9a-f]+\*\/\s*' +
r'vlSelfRef\.__VtrigSched_[\d\w]*\.resume\([\n\s]*\"@\(\[event\]\st\.e\)\"\);')
test.execute(all_run_flags=["+verilator+debug"], expect_filename=test.golden_filename)
test.passes()

View File

@ -0,0 +1,44 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain, for
// any use, without warranty, 2026 by Antmicro.
// SPDX-License-Identifier: CC0-1.0
module t;
logic clk = 1;
logic clk_inv;
event a;
event e;
// This $c is required to prevent inlining clk_inv as ~clk
assign clk_inv = $c(1) & ~clk;
// This is needed to provide right order of resumption in scheduler
initial begin
@a;
@(posedge clk_inv);
@e;
end
initial begin
forever begin
@(posedge clk_inv) begin
clk = 1;
->e;
@e;
end
end
end
initial begin
@a;
@e;
if (clk_inv != 0) $stop;
$finish;
end
initial begin
#1;
->a;
clk = 0;
#2 $stop;
end
endmodule

View File

@ -16,11 +16,11 @@ internalsDump:
-V{t#,#}+ Eval
-V{t#,#}+ Vt_verilated_debug___024root___eval
-V{t#,#}+ Vt_verilated_debug___024root___eval_phase__act
-V{t#,#}+ Vt_verilated_debug___024root___eval_triggers__act
-V{t#,#}+ Vt_verilated_debug___024root___eval_triggers_vec__act
-V{t#,#}+ Vt_verilated_debug___024root___dump_triggers__act
-V{t#,#}+ Vt_verilated_debug___024root___trigger_anySet__act
-V{t#,#} No 'act' region triggers active
-V{t#,#}+ Vt_verilated_debug___024root___trigger_orInto__act
-V{t#,#}+ Vt_verilated_debug___024root___trigger_orInto__act_vec_vec
-V{t#,#}+ Vt_verilated_debug___024root___eval_phase__nba
-V{t#,#}+ Vt_verilated_debug___024root___trigger_anySet__act
-V{t#,#}End-of-eval cleanup
@ -29,22 +29,22 @@ internalsDump:
-V{t#,#}+ Eval
-V{t#,#}+ Vt_verilated_debug___024root___eval
-V{t#,#}+ Vt_verilated_debug___024root___eval_phase__act
-V{t#,#}+ Vt_verilated_debug___024root___eval_triggers__act
-V{t#,#}+ Vt_verilated_debug___024root___eval_triggers_vec__act
-V{t#,#}+ Vt_verilated_debug___024root___dump_triggers__act
-V{t#,#}+ Vt_verilated_debug___024root___trigger_anySet__act
-V{t#,#} 'act' region trigger index 0 is active: @(posedge clk)
-V{t#,#}+ Vt_verilated_debug___024root___trigger_orInto__act
-V{t#,#}+ Vt_verilated_debug___024root___trigger_orInto__act_vec_vec
-V{t#,#}+ Vt_verilated_debug___024root___eval_phase__nba
-V{t#,#}+ Vt_verilated_debug___024root___trigger_anySet__act
-V{t#,#}+ Vt_verilated_debug___024root___eval_nba
*-* All Finished *-*
-V{t#,#}+ Vt_verilated_debug___024root___trigger_clear__act
-V{t#,#}+ Vt_verilated_debug___024root___eval_phase__act
-V{t#,#}+ Vt_verilated_debug___024root___eval_triggers__act
-V{t#,#}+ Vt_verilated_debug___024root___eval_triggers_vec__act
-V{t#,#}+ Vt_verilated_debug___024root___dump_triggers__act
-V{t#,#}+ Vt_verilated_debug___024root___trigger_anySet__act
-V{t#,#} No 'act' region triggers active
-V{t#,#}+ Vt_verilated_debug___024root___trigger_orInto__act
-V{t#,#}+ Vt_verilated_debug___024root___trigger_orInto__act_vec_vec
-V{t#,#}+ Vt_verilated_debug___024root___eval_phase__nba
-V{t#,#}+ Vt_verilated_debug___024root___trigger_anySet__act
-V{t#,#}End-of-eval cleanup