mirror of
https://github.com/verilator/verilator.git
synced 2025-01-01 04:07:34 +00:00
Add std::process class (#4212)
This commit is contained in:
parent
9cc218db3e
commit
db7935faf3
@ -26,10 +26,6 @@
|
||||
// verilator lint_off TIMESCALEMOD
|
||||
// verilator lint_off UNUSEDSIGNAL
|
||||
package std;
|
||||
// The process class is not implemented, but it's predeclared here,
|
||||
// so the linter accepts references to it.
|
||||
typedef class process;
|
||||
|
||||
class mailbox #(type T);
|
||||
protected int m_bound;
|
||||
protected T m_queue[$];
|
||||
@ -45,7 +41,7 @@ package std;
|
||||
task put(T message);
|
||||
`ifdef VERILATOR_TIMING
|
||||
if (m_bound != 0)
|
||||
wait (m_queue.size() < m_bound);
|
||||
wait (m_queue.size() < m_bound);
|
||||
m_queue.push_back(message);
|
||||
`endif
|
||||
endtask
|
||||
@ -117,43 +113,80 @@ package std;
|
||||
endclass
|
||||
|
||||
class process;
|
||||
typedef enum { FINISHED, RUNNING, WAITING, SUSPENDED, KILLED } state;
|
||||
static process _s_global_process;
|
||||
typedef enum {
|
||||
FINISHED = 0,
|
||||
RUNNING = 1,
|
||||
WAITING = 2,
|
||||
SUSPENDED = 3,
|
||||
KILLED = 4
|
||||
} state;
|
||||
|
||||
`ifdef VERILATOR_TIMING
|
||||
// Width visitor changes it to VlProcessRef
|
||||
protected chandle m_process;
|
||||
`endif
|
||||
|
||||
static function process self();
|
||||
// Unsupported, emulating with single process' state
|
||||
if (!_s_global_process) _s_global_process = new;
|
||||
return _s_global_process;
|
||||
process p = new;
|
||||
`ifdef VERILATOR_TIMING
|
||||
$c(p.m_process, " = vlProcess;");
|
||||
`endif
|
||||
return p;
|
||||
endfunction
|
||||
|
||||
protected function void set_status(state s);
|
||||
`ifdef VERILATOR_TIMING
|
||||
$c(m_process, "->state(", s, ");");
|
||||
`endif
|
||||
endfunction
|
||||
|
||||
function state status();
|
||||
// Unsupported, emulating with single process' state
|
||||
`ifdef VERILATOR_TIMING
|
||||
return state'($c(m_process, "->state()"));
|
||||
`else
|
||||
return RUNNING;
|
||||
`endif
|
||||
endfunction
|
||||
|
||||
function void kill();
|
||||
$error("std::process::kill() not supported");
|
||||
set_status(KILLED);
|
||||
endfunction
|
||||
task await();
|
||||
$error("std::process::await() not supported");
|
||||
endtask
|
||||
|
||||
function void suspend();
|
||||
$error("std::process::suspend() not supported");
|
||||
endfunction
|
||||
|
||||
function void resume();
|
||||
$error("std::process::resume() not supported");
|
||||
set_status(RUNNING);
|
||||
endfunction
|
||||
// When really implemented, srandom must operates on the process, but for
|
||||
|
||||
task await();
|
||||
`ifdef VERILATOR_TIMING
|
||||
wait (status() == FINISHED || status() == KILLED);
|
||||
`endif
|
||||
endtask
|
||||
|
||||
// When really implemented, srandom must operate on the process, but for
|
||||
// now rely on the srandom() that is automatically generated for all
|
||||
// classes.
|
||||
//
|
||||
// function void srandom(int seed);
|
||||
// endfunction
|
||||
|
||||
// The methods below work only if set_randstate is never applied to
|
||||
// a state string created before another such string. Full support
|
||||
// could use VlRNG class to store the state per process in VlProcess
|
||||
// objects.
|
||||
function string get_randstate();
|
||||
// Could operate on all proceses for now
|
||||
// No error, as harmless until set_randstate is called
|
||||
return "NOT_SUPPORTED";
|
||||
string s;
|
||||
|
||||
s.itoa($random); // Get a random number
|
||||
set_randstate(s); // Pretend it's the state of RNG
|
||||
return s;
|
||||
endfunction
|
||||
function void set_randstate(string randstate);
|
||||
$error("std::process::set_randstate() not supported");
|
||||
// Could operate on all proceses for now
|
||||
|
||||
function void set_randstate(string s);
|
||||
$urandom(s.atoi()); // Set the seed using a string
|
||||
endfunction
|
||||
endclass
|
||||
|
||||
endpackage
|
||||
|
@ -30,7 +30,16 @@ void VlCoroutineHandle::resume() {
|
||||
// main process
|
||||
if (VL_LIKELY(m_coro)) {
|
||||
VL_DEBUG_IF(VL_DBG_MSGF(" Resuming: "); dump(););
|
||||
m_coro();
|
||||
if (m_process) { // If process state is managed with std::process
|
||||
if (m_process->state() == VlProcess::KILLED) {
|
||||
m_coro.destroy();
|
||||
} else {
|
||||
m_process->state(VlProcess::RUNNING);
|
||||
m_coro();
|
||||
}
|
||||
} else {
|
||||
m_coro();
|
||||
}
|
||||
m_coro = nullptr;
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +86,36 @@ public:
|
||||
#endif
|
||||
};
|
||||
|
||||
//===================================================================
|
||||
// VlProcess stores metadata of running processes
|
||||
|
||||
class VlProcess final {
|
||||
// MEMBERS
|
||||
int m_state; // Current state of the process
|
||||
|
||||
public:
|
||||
// TYPES
|
||||
enum : int { // Type int for compatibility with $c
|
||||
FINISHED = 0,
|
||||
RUNNING = 1,
|
||||
WAITING = 2,
|
||||
SUSPENDED = 3,
|
||||
KILLED = 4,
|
||||
};
|
||||
|
||||
// CONSTRUCTORS
|
||||
VlProcess()
|
||||
: m_state{RUNNING} {}
|
||||
|
||||
// METHODS
|
||||
int state() { return m_state; }
|
||||
void state(int s) { m_state = s; }
|
||||
};
|
||||
|
||||
using VlProcessRef = std::shared_ptr<VlProcess>;
|
||||
|
||||
inline std::string VL_TO_STRING(const VlProcessRef& p) { return std::string("process"); }
|
||||
|
||||
//=============================================================================
|
||||
// VlCoroutineHandle is a non-copyable (but movable) coroutine handle. On resume, the handle is
|
||||
// cleared, as we assume that either the coroutine has finished and deleted itself, or, if it got
|
||||
@ -96,25 +126,38 @@ class VlCoroutineHandle final {
|
||||
|
||||
// MEMBERS
|
||||
std::coroutine_handle<> m_coro; // The wrapped coroutine handle
|
||||
VlProcessRef m_process; // Data of the suspended process, null if not needed
|
||||
VlFileLineDebug m_fileline;
|
||||
|
||||
public:
|
||||
// CONSTRUCTORS
|
||||
// Construct
|
||||
VlCoroutineHandle()
|
||||
: m_coro{nullptr} {}
|
||||
VlCoroutineHandle(std::coroutine_handle<> coro, VlFileLineDebug fileline)
|
||||
VlCoroutineHandle(VlProcessRef process)
|
||||
: m_coro{nullptr}
|
||||
, m_process{process} {
|
||||
if (m_process) m_process->state(VlProcess::WAITING);
|
||||
}
|
||||
VlCoroutineHandle(std::coroutine_handle<> coro, VlProcessRef process, VlFileLineDebug fileline)
|
||||
: m_coro{coro}
|
||||
, m_fileline{fileline} {}
|
||||
, m_process{process}
|
||||
, m_fileline{fileline} {
|
||||
if (m_process) m_process->state(VlProcess::WAITING);
|
||||
}
|
||||
// Move the handle, leaving a nullptr
|
||||
VlCoroutineHandle(VlCoroutineHandle&& moved)
|
||||
: m_coro{std::exchange(moved.m_coro, nullptr)}
|
||||
, m_process{moved.m_process}
|
||||
, m_fileline{moved.m_fileline} {}
|
||||
// Destroy if the handle isn't null
|
||||
~VlCoroutineHandle() {
|
||||
// Usually these coroutines should get resumed; we only need to clean up if we destroy a
|
||||
// model with some coroutines suspended
|
||||
if (VL_UNLIKELY(m_coro)) m_coro.destroy();
|
||||
if (VL_UNLIKELY(m_coro)) {
|
||||
m_coro.destroy();
|
||||
if (m_process && m_process->state() != VlProcess::KILLED) {
|
||||
m_process->state(VlProcess::FINISHED);
|
||||
}
|
||||
}
|
||||
}
|
||||
// METHODS
|
||||
// Move the handle, leaving a null handle
|
||||
@ -122,7 +165,7 @@ public:
|
||||
m_coro = std::exchange(moved.m_coro, nullptr);
|
||||
return *this;
|
||||
}
|
||||
// Resume the coroutine if the handle isn't null
|
||||
// Resume the coroutine if the handle isn't null and the process isn't killed
|
||||
void resume();
|
||||
#ifdef VL_DEBUG
|
||||
void dump() const;
|
||||
@ -173,21 +216,24 @@ public:
|
||||
void dump() const;
|
||||
#endif
|
||||
// Used by coroutines for co_awaiting a certain simulation time
|
||||
auto delay(uint64_t delay, const char* filename = VL_UNKNOWN, int lineno = 0) {
|
||||
auto delay(uint64_t delay, VlProcessRef process, const char* filename = VL_UNKNOWN,
|
||||
int lineno = 0) {
|
||||
struct Awaitable {
|
||||
VlProcessRef process; // Data of the suspended process, null if not needed
|
||||
VlDelayedCoroutineQueue& queue;
|
||||
uint64_t delay;
|
||||
VlFileLineDebug fileline;
|
||||
|
||||
bool await_ready() const { return false; } // Always suspend
|
||||
void await_suspend(std::coroutine_handle<> coro) {
|
||||
queue.push_back({delay, VlCoroutineHandle{coro, fileline}});
|
||||
queue.push_back({delay, VlCoroutineHandle{coro, process, fileline}});
|
||||
// Move last element to the proper place in the max-heap
|
||||
std::push_heap(queue.begin(), queue.end());
|
||||
}
|
||||
void await_resume() const {}
|
||||
};
|
||||
return Awaitable{m_queue, m_context.time() + delay, VlFileLineDebug{filename, lineno}};
|
||||
return Awaitable{process, m_queue, m_context.time() + delay,
|
||||
VlFileLineDebug{filename, lineno}};
|
||||
}
|
||||
};
|
||||
|
||||
@ -224,21 +270,23 @@ public:
|
||||
void dump(const char* eventDescription) const;
|
||||
#endif
|
||||
// Used by coroutines for co_awaiting a certain trigger
|
||||
auto trigger(bool commit, const char* eventDescription = VL_UNKNOWN,
|
||||
auto trigger(bool commit, 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););
|
||||
struct Awaitable {
|
||||
VlCoroutineVec& suspended; // Coros waiting on trigger
|
||||
VlProcessRef process; // Data of the suspended process, null if not needed
|
||||
VlFileLineDebug fileline;
|
||||
|
||||
bool await_ready() const { return false; } // Always suspend
|
||||
void await_suspend(std::coroutine_handle<> coro) {
|
||||
suspended.emplace_back(coro, fileline);
|
||||
suspended.emplace_back(coro, process, fileline);
|
||||
}
|
||||
void await_resume() const {}
|
||||
};
|
||||
return Awaitable{commit ? m_ready : m_uncommitted, VlFileLineDebug{filename, lineno}};
|
||||
return Awaitable{commit ? m_ready : m_uncommitted, process,
|
||||
VlFileLineDebug{filename, lineno}};
|
||||
}
|
||||
};
|
||||
|
||||
@ -271,18 +319,19 @@ class VlDynamicTriggerScheduler final {
|
||||
// with destructive post updates, e.g. named events)
|
||||
|
||||
// METHODS
|
||||
auto awaitable(VlCoroutineVec& queue, const char* filename, int lineno) {
|
||||
auto awaitable(VlProcessRef process, VlCoroutineVec& queue, const char* filename, int lineno) {
|
||||
struct Awaitable {
|
||||
VlProcessRef process; // Data of the suspended process, null if not needed
|
||||
VlCoroutineVec& suspended; // Coros waiting on trigger
|
||||
VlFileLineDebug fileline;
|
||||
|
||||
bool await_ready() const { return false; } // Always suspend
|
||||
void await_suspend(std::coroutine_handle<> coro) {
|
||||
suspended.emplace_back(coro, fileline);
|
||||
suspended.emplace_back(coro, process, fileline);
|
||||
}
|
||||
void await_resume() const {}
|
||||
};
|
||||
return Awaitable{queue, VlFileLineDebug{filename, lineno}};
|
||||
return Awaitable{process, queue, VlFileLineDebug{filename, lineno}};
|
||||
}
|
||||
|
||||
public:
|
||||
@ -296,23 +345,26 @@ public:
|
||||
void dump() const;
|
||||
#endif
|
||||
// Used by coroutines for co_awaiting trigger evaluation
|
||||
auto evaluation(const char* eventDescription, const char* filename, int lineno) {
|
||||
auto evaluation(VlProcessRef process, const char* eventDescription, const char* filename,
|
||||
int lineno) {
|
||||
VL_DEBUG_IF(VL_DBG_MSGF(" Suspending process waiting for %s at %s:%d\n",
|
||||
eventDescription, filename, lineno););
|
||||
return awaitable(m_suspended, filename, lineno);
|
||||
return awaitable(process, m_suspended, filename, lineno);
|
||||
}
|
||||
// Used by coroutines for co_awaiting the trigger post update step
|
||||
auto postUpdate(const char* eventDescription, const char* filename, int lineno) {
|
||||
auto postUpdate(VlProcessRef process, const char* eventDescription, const char* filename,
|
||||
int lineno) {
|
||||
VL_DEBUG_IF(
|
||||
VL_DBG_MSGF(" Process waiting for %s at %s:%d awaiting the post update step\n",
|
||||
eventDescription, filename, lineno););
|
||||
return awaitable(m_post, filename, lineno);
|
||||
return awaitable(process, m_post, filename, lineno);
|
||||
}
|
||||
// Used by coroutines for co_awaiting the resumption step (in 'act' eval)
|
||||
auto resumption(const char* eventDescription, const char* filename, int lineno) {
|
||||
auto resumption(VlProcessRef process, const char* eventDescription, const char* filename,
|
||||
int lineno) {
|
||||
VL_DEBUG_IF(VL_DBG_MSGF(" Process waiting for %s at %s:%d awaiting resumption\n",
|
||||
eventDescription, filename, lineno););
|
||||
return awaitable(m_triggered, filename, lineno);
|
||||
return awaitable(process, m_triggered, filename, lineno);
|
||||
}
|
||||
};
|
||||
|
||||
@ -342,24 +394,27 @@ class VlForkSync final {
|
||||
|
||||
public:
|
||||
// Create the join object and set the counter to the specified number
|
||||
void init(size_t count) { m_join.reset(new VlJoin{count, {}}); }
|
||||
void init(size_t count, VlProcessRef process) { m_join.reset(new VlJoin{count, {process}}); }
|
||||
// Called whenever any of the forked processes finishes. If the join counter reaches 0, the
|
||||
// main process gets resumed
|
||||
void done(const char* filename = VL_UNKNOWN, int lineno = 0);
|
||||
// Used by coroutines for co_awaiting a join
|
||||
auto join(const char* filename = VL_UNKNOWN, int lineno = 0) {
|
||||
auto join(VlProcessRef process, const char* filename = VL_UNKNOWN, int lineno = 0) {
|
||||
assert(m_join);
|
||||
VL_DEBUG_IF(
|
||||
VL_DBG_MSGF(" Awaiting join of fork at: %s:%d\n", filename, lineno););
|
||||
struct Awaitable {
|
||||
VlProcessRef process; // Data of the suspended process, null if not needed
|
||||
const std::shared_ptr<VlJoin> join; // Join to await on
|
||||
VlFileLineDebug fileline;
|
||||
|
||||
bool await_ready() { return join->m_counter == 0; } // Suspend if join still exists
|
||||
void await_suspend(std::coroutine_handle<> coro) { join->m_susp = {coro, fileline}; }
|
||||
void await_suspend(std::coroutine_handle<> coro) {
|
||||
join->m_susp = {coro, process, fileline};
|
||||
}
|
||||
void await_resume() const {}
|
||||
};
|
||||
return Awaitable{m_join, VlFileLineDebug{filename, lineno}};
|
||||
return Awaitable{process, m_join, VlFileLineDebug{filename, lineno}};
|
||||
}
|
||||
};
|
||||
|
||||
|
26
src/V3Ast.h
26
src/V3Ast.h
@ -461,6 +461,7 @@ public:
|
||||
TRIGGER_SCHEDULER,
|
||||
DYNAMIC_TRIGGER_SCHEDULER,
|
||||
FORK_SYNC,
|
||||
PROCESS_REFERENCE,
|
||||
// Unsigned and two state; fundamental types
|
||||
UINT32,
|
||||
UINT64,
|
||||
@ -493,6 +494,7 @@ public:
|
||||
"VlTriggerScheduler",
|
||||
"VlDynamicTriggerScheduler",
|
||||
"VlFork",
|
||||
"VlProcessRef",
|
||||
"IData",
|
||||
"QData",
|
||||
"LOGIC_IMPLICIT",
|
||||
@ -500,13 +502,20 @@ public:
|
||||
return names[m_e];
|
||||
}
|
||||
const char* dpiType() const {
|
||||
static const char* const names[]
|
||||
= {"%E-unk", "svBit", "char", "void*", "char",
|
||||
"int", "%E-integer", "svLogic", "long long", "double",
|
||||
"short", "%E-time", "const char*", "%E-untyped", "dpiScope",
|
||||
"const char*", "%E-mtaskstate", "%E-triggervec", "%E-dly-sched", "%E-trig-sched",
|
||||
"%E-dyn-sched", "%E-fork", "IData", "QData", "%E-logic-implct",
|
||||
" MAX"};
|
||||
static const char* const names[] = {"%E-unk", "svBit",
|
||||
"char", "void*",
|
||||
"char", "int",
|
||||
"%E-integer", "svLogic",
|
||||
"long long", "double",
|
||||
"short", "%E-time",
|
||||
"const char*", "%E-untyped",
|
||||
"dpiScope", "const char*",
|
||||
"%E-mtaskstate", "%E-triggervec",
|
||||
"%E-dly-sched", "%E-trig-sched",
|
||||
"%E-dyn-sched", "%E-fork",
|
||||
"%E-proc-ref", "IData",
|
||||
"QData", "%E-logic-implct",
|
||||
" MAX"};
|
||||
return names[m_e];
|
||||
}
|
||||
static void selfTest() {
|
||||
@ -545,6 +554,7 @@ public:
|
||||
case TRIGGER_SCHEDULER: return 0; // opaque
|
||||
case DYNAMIC_TRIGGER_SCHEDULER: return 0; // opaque
|
||||
case FORK_SYNC: return 0; // opaque
|
||||
case PROCESS_REFERENCE: return 0; // opaque
|
||||
case UINT32: return 32;
|
||||
case UINT64: return 64;
|
||||
default: return 0;
|
||||
@ -584,7 +594,7 @@ public:
|
||||
return (m_e == EVENT || m_e == STRING || m_e == SCOPEPTR || m_e == CHARPTR
|
||||
|| m_e == MTASKSTATE || m_e == TRIGGERVEC || m_e == DELAY_SCHEDULER
|
||||
|| m_e == TRIGGER_SCHEDULER || m_e == DYNAMIC_TRIGGER_SCHEDULER || m_e == FORK_SYNC
|
||||
|| m_e == DOUBLE || m_e == UNTYPED);
|
||||
|| m_e == PROCESS_REFERENCE || m_e == DOUBLE || m_e == UNTYPED);
|
||||
}
|
||||
bool isDouble() const VL_MT_SAFE { return m_e == DOUBLE; }
|
||||
bool isEvent() const { return m_e == EVENT; }
|
||||
|
@ -438,6 +438,7 @@ public:
|
||||
bool isEvent() const VL_MT_SAFE { return keyword() == VBasicDTypeKwd::EVENT; }
|
||||
bool isTriggerVec() const VL_MT_SAFE { return keyword() == VBasicDTypeKwd::TRIGGERVEC; }
|
||||
bool isForkSync() const VL_MT_SAFE { return keyword() == VBasicDTypeKwd::FORK_SYNC; }
|
||||
bool isProcessRef() const VL_MT_SAFE { return keyword() == VBasicDTypeKwd::PROCESS_REFERENCE; }
|
||||
bool isDelayScheduler() const VL_MT_SAFE {
|
||||
return keyword() == VBasicDTypeKwd::DELAY_SCHEDULER;
|
||||
}
|
||||
|
@ -86,6 +86,7 @@ private:
|
||||
bool m_recursive : 1; // Recursive or part of recursion
|
||||
bool m_underGenerate : 1; // Under generate (for warning)
|
||||
bool m_virtual : 1; // Virtual method in class
|
||||
bool m_fromStd : 1; // Part of std
|
||||
VLifetime m_lifetime; // Lifetime
|
||||
protected:
|
||||
AstNodeFTask(VNType t, FileLine* fl, const string& name, AstNode* stmtsp)
|
||||
@ -110,7 +111,8 @@ protected:
|
||||
, m_pureVirtual{false}
|
||||
, m_recursive{false}
|
||||
, m_underGenerate{false}
|
||||
, m_virtual{false} {
|
||||
, m_virtual{false}
|
||||
, m_fromStd{false} {
|
||||
addStmtsp(stmtsp);
|
||||
cname(name); // Might be overridden by dpi import/export
|
||||
}
|
||||
@ -170,6 +172,8 @@ public:
|
||||
bool underGenerate() const { return m_underGenerate; }
|
||||
void isVirtual(bool flag) { m_virtual = flag; }
|
||||
bool isVirtual() const { return m_virtual; }
|
||||
void isFromStd(bool flag) { m_fromStd = flag; }
|
||||
bool isFromStd() const { return m_fromStd; }
|
||||
void lifetime(const VLifetime& flag) { m_lifetime = flag; }
|
||||
VLifetime lifetime() const { return m_lifetime; }
|
||||
bool isFirstInMyListOfStatements(AstNode* n) const override { return n == stmtsp(); }
|
||||
@ -272,10 +276,13 @@ public:
|
||||
class AstNodeProcedure VL_NOT_FINAL : public AstNode {
|
||||
// IEEE procedure: initial, final, always
|
||||
// @astgen op2 := stmtsp : List[AstNode] // Note: op1 is used in some sub-types only
|
||||
bool m_suspendable = false; // Is suspendable by a Delay, EventControl, etc.
|
||||
bool m_suspendable : 1; // Is suspendable by a Delay, EventControl, etc.
|
||||
bool m_needProcess : 1; // Implements part of a process that allocates std::process
|
||||
protected:
|
||||
AstNodeProcedure(VNType t, FileLine* fl, AstNode* stmtsp)
|
||||
: AstNode{t, fl} {
|
||||
m_needProcess = false;
|
||||
m_suspendable = false;
|
||||
addStmtsp(stmtsp);
|
||||
}
|
||||
|
||||
@ -286,6 +293,8 @@ public:
|
||||
bool isJustOneBodyStmt() const { return stmtsp() && !stmtsp()->nextp(); }
|
||||
bool isSuspendable() const { return m_suspendable; }
|
||||
void setSuspendable() { m_suspendable = true; }
|
||||
bool needProcess() const { return m_needProcess; }
|
||||
void setNeedProcess() { m_needProcess = true; }
|
||||
};
|
||||
class AstNodeRange VL_NOT_FINAL : public AstNode {
|
||||
// A range, sized or unsized
|
||||
@ -576,6 +585,7 @@ private:
|
||||
bool m_dpiImportPrototype : 1; // This is the DPI import prototype (i.e.: provided by user)
|
||||
bool m_dpiImportWrapper : 1; // Wrapper for invoking DPI import prototype from generated code
|
||||
bool m_dpiTraceInit : 1; // DPI trace_init
|
||||
bool m_needProcess : 1; // Implements part of a process that allocates std::process
|
||||
public:
|
||||
AstCFunc(FileLine* fl, const string& name, AstScope* scopep, const string& rtnType = "")
|
||||
: ASTGEN_SUPER_CFunc(fl) {
|
||||
@ -595,6 +605,7 @@ public:
|
||||
m_isLoose = false;
|
||||
m_isInline = false;
|
||||
m_isVirtual = false;
|
||||
m_needProcess = false;
|
||||
m_entryPoint = false;
|
||||
m_pure = false;
|
||||
m_dpiContext = false;
|
||||
@ -663,6 +674,8 @@ public:
|
||||
void isInline(bool flag) { m_isInline = flag; }
|
||||
bool isVirtual() const { return m_isVirtual; }
|
||||
void isVirtual(bool flag) { m_isVirtual = flag; }
|
||||
bool needProcess() const { return m_needProcess; }
|
||||
void setNeedProcess() { m_needProcess = true; }
|
||||
bool entryPoint() const { return m_entryPoint; }
|
||||
void entryPoint(bool flag) { m_entryPoint = flag; }
|
||||
bool pure() const { return m_pure; }
|
||||
|
@ -784,6 +784,8 @@ AstNodeDType::CTypeRecursed AstNodeDType::cTypeRecurse(bool compound) const VL_M
|
||||
info.m_type = "VlDynamicTriggerScheduler";
|
||||
} else if (bdtypep->isForkSync()) {
|
||||
info.m_type = "VlForkSync";
|
||||
} else if (bdtypep->isProcessRef()) {
|
||||
info.m_type = "VlProcessRef";
|
||||
} else if (bdtypep->isEvent()) {
|
||||
info.m_type = "VlEvent";
|
||||
} else if (dtypep->widthMin() <= 8) { // Handle unpacked arrays; not bdtypep->width
|
||||
@ -1352,6 +1354,7 @@ void AstNode::dump(std::ostream& str) const {
|
||||
void AstNodeProcedure::dump(std::ostream& str) const {
|
||||
this->AstNode::dump(str);
|
||||
if (isSuspendable()) str << " [SUSP]";
|
||||
if (needProcess()) str << " [NPRC]";
|
||||
}
|
||||
|
||||
void AstAlways::dump(std::ostream& str) const {
|
||||
@ -2294,6 +2297,7 @@ void AstCFunc::dump(std::ostream& str) const {
|
||||
if (isDestructor()) str << " [DTOR]";
|
||||
if (isVirtual()) str << " [VIRT]";
|
||||
if (isCoroutine()) str << " [CORO]";
|
||||
if (needProcess()) str << " [NPRC]";
|
||||
}
|
||||
const char* AstCAwait::broken() const {
|
||||
BROKEN_RTN(m_sensesp && !m_sensesp->brokeExists());
|
||||
|
@ -186,7 +186,8 @@ private:
|
||||
&& !VN_IS(backp, NodeCCall) && !VN_IS(backp, CMethodHard) && !VN_IS(backp, SFormatF)
|
||||
&& !VN_IS(backp, ArraySel) && !VN_IS(backp, StructSel) && !VN_IS(backp, RedXor)
|
||||
&& (nodep->varp()->basicp() && !nodep->varp()->basicp()->isTriggerVec()
|
||||
&& !nodep->varp()->basicp()->isForkSync())
|
||||
&& !nodep->varp()->basicp()->isForkSync()
|
||||
&& !nodep->varp()->basicp()->isProcessRef())
|
||||
&& backp->width() && castSize(nodep) != castSize(nodep->varp())) {
|
||||
// Cast vars to IData first, else below has upper bits wrongly set
|
||||
// CData x=3; out = (QData)(x<<30);
|
||||
|
@ -79,6 +79,10 @@ string EmitCBaseVisitorConst::cFuncArgs(const AstCFunc* nodep) {
|
||||
args += prefixNameProtect(EmitCParentModule::get(nodep));
|
||||
args += "* vlSelf";
|
||||
}
|
||||
if (nodep->needProcess()) {
|
||||
if (!args.empty()) args += ", ";
|
||||
args += "VlProcessRef vlProcess";
|
||||
}
|
||||
if (!nodep->argTypes().empty()) {
|
||||
if (!args.empty()) args += ", ";
|
||||
args += nodep->argTypes();
|
||||
|
@ -417,6 +417,15 @@ void EmitCFunc::emitCCallArgs(const AstNodeCCall* nodep, const string& selfPoint
|
||||
puts(selfPointer);
|
||||
comma = true;
|
||||
}
|
||||
if (nodep->funcp()->needProcess()) {
|
||||
if (comma) puts(", ");
|
||||
if (VN_IS(nodep->backp(), CAwait)) {
|
||||
puts("vlProcess");
|
||||
} else {
|
||||
puts("std::make_shared<VlProcess>()");
|
||||
}
|
||||
comma = true;
|
||||
}
|
||||
if (!nodep->argTypes().empty()) {
|
||||
if (comma) puts(", ");
|
||||
puts(nodep->argTypes());
|
||||
@ -695,6 +704,8 @@ string EmitCFunc::emitVarResetRecurse(const AstVar* varp, const string& varNameP
|
||||
return "";
|
||||
} else if (basicp && basicp->isForkSync()) {
|
||||
return "";
|
||||
} else if (basicp && basicp->isProcessRef()) {
|
||||
return "";
|
||||
} else if (basicp && basicp->isDelayScheduler()) {
|
||||
return "";
|
||||
} else if (basicp && basicp->isTriggerScheduler()) {
|
||||
|
@ -1214,9 +1214,11 @@ AstActive* OrderProcess::processMoveOneLogic(const OrderLogicVertex* lvertexp,
|
||||
// Process procedures per statement (unless profCFuncs), so we can split CFuncs within
|
||||
// procedures. Everything else is handled in one go
|
||||
bool suspendable = false;
|
||||
bool needProcess = false;
|
||||
bool slow = m_slow;
|
||||
if (AstNodeProcedure* const procp = VN_CAST(nodep, NodeProcedure)) {
|
||||
suspendable = procp->isSuspendable();
|
||||
needProcess = procp->needProcess();
|
||||
if (suspendable) slow = slow && !VN_IS(procp, Always);
|
||||
nodep = procp->stmtsp();
|
||||
pushDeletep(procp);
|
||||
@ -1241,6 +1243,7 @@ AstActive* OrderProcess::processMoveOneLogic(const OrderLogicVertex* lvertexp,
|
||||
const string name = cfuncName(modp, domainp, scopep, nodep);
|
||||
newFuncpr
|
||||
= new AstCFunc{nodep->fileline(), name, scopep, suspendable ? "VlCoroutine" : ""};
|
||||
if (needProcess) newFuncpr->setNeedProcess();
|
||||
newFuncpr->isStatic(false);
|
||||
newFuncpr->isLoose(true);
|
||||
newFuncpr->slow(slow);
|
||||
|
@ -247,6 +247,7 @@ void orderSequentially(AstCFunc* funcp, const LogicByScope& lbs) {
|
||||
subFuncp = createNewSubFuncp(scopep);
|
||||
subFuncp->name(subFuncp->name() + "__" + cvtToStr(scopep->user2Inc()));
|
||||
subFuncp->rtnType("VlCoroutine");
|
||||
if (procp->needProcess()) subFuncp->setNeedProcess();
|
||||
if (VN_IS(procp, Always)) {
|
||||
subFuncp->slow(false);
|
||||
FileLine* const flp = procp->fileline();
|
||||
|
@ -155,6 +155,19 @@ TimingKit prepareTiming(AstNetlist* const netlistp) {
|
||||
std::vector<AstVarScope*> m_writtenBySuspendable;
|
||||
|
||||
// METHODS
|
||||
// Add arguments to a resume() call based on arguments in the suspending call
|
||||
void addResumePins(AstCMethodHard* const resumep, AstNodeExpr* pinsp) {
|
||||
AstCExpr* const exprp = VN_CAST(pinsp, CExpr);
|
||||
AstText* const textp = VN_CAST(exprp->exprsp(), Text);
|
||||
if (textp) {
|
||||
// The first argument, vlProcess, isn't used by any of resume() methods, skip it
|
||||
if ((pinsp = VN_CAST(pinsp->nextp(), NodeExpr))) {
|
||||
resumep->addPinsp(pinsp->cloneTree(false));
|
||||
}
|
||||
} else {
|
||||
resumep->addPinsp(pinsp->cloneTree(false));
|
||||
}
|
||||
}
|
||||
// Create an active with a timing scheduler resume() call
|
||||
void createResumeActive(AstCAwait* const awaitp) {
|
||||
auto* const methodp = VN_AS(awaitp->exprp(), CMethodHard);
|
||||
@ -171,7 +184,8 @@ TimingKit prepareTiming(AstNetlist* const netlistp) {
|
||||
// The first pin is the commit boolean, the rest (if any) should be debug info
|
||||
// See V3Timing for details
|
||||
if (AstNode* const dbginfop = methodp->pinsp()->nextp()) {
|
||||
resumep->addPinsp(static_cast<AstNodeExpr*>(dbginfop)->cloneTree(false));
|
||||
if (methodp->pinsp())
|
||||
addResumePins(resumep, static_cast<AstNodeExpr*>(dbginfop));
|
||||
}
|
||||
} else if (schedulerp->dtypep()->basicp()->isDynamicTriggerScheduler()) {
|
||||
auto* const postp = resumep->cloneTree(false);
|
||||
@ -262,6 +276,7 @@ void transformForks(AstNetlist* const netlistp) {
|
||||
// STATE
|
||||
bool m_inClass = false; // Are we in a class?
|
||||
bool m_beginHasAwaits = false; // Does the current begin have awaits?
|
||||
bool m_beginNeedProcess = false; // Does the current begin have process::self dependency?
|
||||
AstFork* m_forkp = nullptr; // Current fork
|
||||
AstCFunc* m_funcp = nullptr; // Current function
|
||||
|
||||
@ -348,8 +363,9 @@ void transformForks(AstNetlist* const netlistp) {
|
||||
UASSERT_OBJ(m_forkp, nodep, "Begin outside of a fork");
|
||||
// Start with children, so later we only find awaits that are actually in this begin
|
||||
m_beginHasAwaits = false;
|
||||
m_beginNeedProcess = false;
|
||||
iterateChildrenConst(nodep);
|
||||
if (m_beginHasAwaits) {
|
||||
if (m_beginHasAwaits || m_beginNeedProcess) {
|
||||
UASSERT_OBJ(!nodep->name().empty(), nodep, "Begin needs a name");
|
||||
// Create a function to put this begin's statements in
|
||||
FileLine* const flp = nodep->fileline();
|
||||
@ -372,9 +388,19 @@ void transformForks(AstNetlist* const netlistp) {
|
||||
}
|
||||
// Put the begin's statements in the function, delete the begin
|
||||
newfuncp->addStmtsp(nodep->stmtsp()->unlinkFrBackWithNext());
|
||||
if (m_beginNeedProcess) {
|
||||
newfuncp->setNeedProcess();
|
||||
newfuncp->addStmtsp(new AstCStmt{nodep->fileline(),
|
||||
"vlProcess->state(VlProcess::FINISHED);\n"});
|
||||
}
|
||||
if (!m_beginHasAwaits) {
|
||||
// co_return at the end (either that or a co_await is required in a coroutine
|
||||
newfuncp->addStmtsp(new AstCStmt{nodep->fileline(), "co_return;\n"});
|
||||
}
|
||||
remapLocals(newfuncp, callp);
|
||||
} else {
|
||||
// No awaits, just inline the forked process
|
||||
// The begin has neither awaits nor a process::self call, just inline the
|
||||
// statements
|
||||
nodep->replaceWith(nodep->stmtsp()->unlinkFrBackWithNext());
|
||||
}
|
||||
VL_DO_DANGLING(nodep->deleteTree(), nodep);
|
||||
@ -383,6 +409,10 @@ void transformForks(AstNetlist* const netlistp) {
|
||||
m_beginHasAwaits = true;
|
||||
iterateChildrenConst(nodep);
|
||||
}
|
||||
void visit(AstCCall* nodep) override {
|
||||
if (nodep->funcp()->needProcess()) m_beginNeedProcess = true;
|
||||
iterateChildrenConst(nodep);
|
||||
}
|
||||
|
||||
//--------------------
|
||||
void visit(AstNodeExpr*) override {} // Accelerate
|
||||
|
@ -1316,6 +1316,9 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the fact that this function allocates std::process
|
||||
if (nodep->isFromStd() && nodep->name() == "self") cfuncp->setNeedProcess();
|
||||
|
||||
// Delete rest of cloned task and return new func
|
||||
VL_DO_DANGLING(pushDeletep(nodep), nodep);
|
||||
if (debug() >= 9) cfuncp->dumpTree("- userFunc: ");
|
||||
|
@ -61,6 +61,16 @@
|
||||
|
||||
VL_DEFINE_DEBUG_FUNCTIONS;
|
||||
|
||||
// ######################################################################
|
||||
|
||||
enum TimingFlag : uint8_t {
|
||||
// Properties of flags with higher numbers include properties of flags with
|
||||
// lower numbers
|
||||
T_NORM = 0, // Normal non-suspendable process
|
||||
T_SUSP = 1, // Suspendable
|
||||
T_PROC = 2 // Suspendable with process metadata
|
||||
};
|
||||
|
||||
// ######################################################################
|
||||
// Detect nodes affected by timing
|
||||
|
||||
@ -94,8 +104,10 @@ private:
|
||||
// AstClass::user1() -> bool. Set true if the class
|
||||
// member cache has been
|
||||
// refreshed.
|
||||
// Ast{NodeProcedure,CFunc,Begin}::user2() -> bool. Set true if process/task is
|
||||
// suspendable
|
||||
// Ast{NodeProcedure,CFunc,Begin}::user2() -> int. Set to >= T_SUSP if
|
||||
// process/task suspendable
|
||||
// and to T_PROC if it
|
||||
// needs process metadata.
|
||||
// Ast{NodeProcedure,CFunc,Begin}::user3() -> DependencyVertex*. Vertex in m_depGraph
|
||||
const VNUser1InUse m_user1InUse;
|
||||
const VNUser2InUse m_user2InUse;
|
||||
@ -113,15 +125,23 @@ private:
|
||||
if (!nodep->user3p()) nodep->user3p(new TimingDependencyVertex{&m_depGraph, nodep});
|
||||
return nodep->user3u().to<TimingDependencyVertex*>();
|
||||
}
|
||||
// Propagate suspendable flag to all nodes that depend on the given one
|
||||
void propagateSuspendable(TimingDependencyVertex* const vxp) {
|
||||
// Set timing flag of a node
|
||||
bool setTimingFlag(AstNode* nodep, int flag) {
|
||||
// Properties of flags with higher numbers include properties of flags with lower
|
||||
// numbers, so modify nodep->user2() only if it will increase.
|
||||
if (nodep->user2() < flag) {
|
||||
nodep->user2(flag);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Propagate suspendable/needProcess flag to all nodes that depend on the given one
|
||||
void propagateTimingFlags(TimingDependencyVertex* const vxp) {
|
||||
auto* const parentp = vxp->nodep();
|
||||
for (V3GraphEdge* edgep = vxp->inBeginp(); edgep; edgep = edgep->inNextp()) {
|
||||
auto* const depVxp = static_cast<TimingDependencyVertex*>(edgep->fromp());
|
||||
AstNode* const depp = depVxp->nodep();
|
||||
if (!depp->user2()) {
|
||||
depp->user2(true);
|
||||
propagateSuspendable(depVxp);
|
||||
}
|
||||
if (setTimingFlag(depp, parentp->user2())) propagateTimingFlags(depVxp);
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,6 +162,7 @@ private:
|
||||
m_procp = nodep;
|
||||
iterateChildren(nodep);
|
||||
TimingDependencyVertex* const vxp = getDependencyVertex(nodep);
|
||||
if (nodep->needProcess()) nodep->user2(T_PROC);
|
||||
if (!m_classp) return;
|
||||
// If class method (possibly overrides another method)
|
||||
if (!m_classp->user1SetOnce()) m_classp->repairCache();
|
||||
@ -162,15 +183,13 @@ private:
|
||||
// the root of the inheritance hierarchy and check if the original method is
|
||||
// virtual or not.
|
||||
if (!cextp->classp()->user1SetOnce()) cextp->classp()->repairCache();
|
||||
if (AstCFunc* const overriddenp
|
||||
if (auto* const overriddenp
|
||||
= VN_CAST(cextp->classp()->findMember(nodep->name()), CFunc)) {
|
||||
if (overriddenp->user2()) { // If it's suspendable
|
||||
nodep->user2(true); // Then we are also suspendable
|
||||
// As both are suspendable already, there is no need to add it as our
|
||||
// dependency or self to its dependencies
|
||||
} else {
|
||||
// Make a dependency cycle, as being suspendable should propagate both
|
||||
// up and down the inheritance tree
|
||||
setTimingFlag(nodep, overriddenp->user2());
|
||||
if (nodep->user2()
|
||||
< T_PROC) { // Add a vertex only if the flag can still change
|
||||
// Make a dependency cycle, as being suspendable should propagate both up
|
||||
// and down the inheritance tree
|
||||
TimingDependencyVertex* const overriddenVxp
|
||||
= getDependencyVertex(overriddenp);
|
||||
new V3GraphEdge{&m_depGraph, vxp, overriddenVxp, 1};
|
||||
@ -184,11 +203,8 @@ private:
|
||||
}
|
||||
}
|
||||
void visit(AstNodeCCall* nodep) override {
|
||||
if (nodep->funcp()->user2()) {
|
||||
m_procp->user2(true);
|
||||
// Both the caller and the callee are suspendable, no need to make dependency edges
|
||||
// between them
|
||||
} else {
|
||||
setTimingFlag(m_procp, nodep->funcp()->user2());
|
||||
if (m_procp->user2() < T_PROC) { // Add a vertex only if the flag can still change
|
||||
TimingDependencyVertex* const procVxp = getDependencyVertex(m_procp);
|
||||
TimingDependencyVertex* const funcVxp = getDependencyVertex(nodep->funcp());
|
||||
new V3GraphEdge{&m_depGraph, procVxp, funcVxp, 1};
|
||||
@ -203,7 +219,7 @@ private:
|
||||
void visit(AstNode* nodep) override {
|
||||
if (nodep->isTimingControl()) {
|
||||
v3Global.setUsesTiming();
|
||||
if (m_procp) m_procp->user2(true);
|
||||
if (m_procp) m_procp->user2(T_SUSP);
|
||||
}
|
||||
iterateChildren(nodep);
|
||||
}
|
||||
@ -218,7 +234,7 @@ public:
|
||||
m_depGraph.removeTransitiveEdges();
|
||||
for (V3GraphVertex* vxp = m_depGraph.verticesBeginp(); vxp; vxp = vxp->verticesNextp()) {
|
||||
TimingDependencyVertex* const depVxp = static_cast<TimingDependencyVertex*>(vxp);
|
||||
if (depVxp->nodep()->user2()) propagateSuspendable(depVxp);
|
||||
if (depVxp->nodep()->user2()) propagateTimingFlags(depVxp);
|
||||
}
|
||||
if (dumpGraphLevel() >= 6) m_depGraph.dumpDotFilePrefixed("timing_deps");
|
||||
}
|
||||
@ -252,6 +268,7 @@ private:
|
||||
AstClass* m_classp = nullptr; // Current class
|
||||
AstScope* m_scopep = nullptr; // Current scope
|
||||
AstActive* m_activep = nullptr; // Current active
|
||||
AstNode* m_procp = nullptr; // NodeProcedure/CFunc/Begin we're under
|
||||
double m_timescaleFactor = 1.0; // Factor to scale delays by
|
||||
|
||||
// Unique names
|
||||
@ -460,6 +477,14 @@ private:
|
||||
methodp->addPinsp(createEventDescription(sensesp));
|
||||
addDebugInfo(methodp);
|
||||
}
|
||||
// Adds process pointer to a hardcoded method call
|
||||
void addProcessInfo(AstCMethodHard* const methodp) const {
|
||||
FileLine* const flp = methodp->fileline();
|
||||
AstCExpr* const ap = new AstCExpr{
|
||||
flp, m_procp && m_procp->user2() == T_PROC ? "vlProcess" : "nullptr", 0};
|
||||
ap->dtypeSetVoid();
|
||||
methodp->addPinsp(ap);
|
||||
}
|
||||
// Creates the fork handle type and returns it
|
||||
AstBasicDType* getCreateForkSyncDTypep() {
|
||||
if (m_forkDtp) return m_forkDtp;
|
||||
@ -512,11 +537,13 @@ private:
|
||||
auto* const initp = new AstCMethodHard{flp, new AstVarRef{flp, forkVscp, VAccess::WRITE},
|
||||
"init", new AstConst{flp, joinCount}};
|
||||
initp->dtypeSetVoid();
|
||||
addProcessInfo(initp);
|
||||
forkp->addHereThisAsNext(initp->makeStmt());
|
||||
// Await the join at the end
|
||||
auto* const joinp
|
||||
= new AstCMethodHard{flp, new AstVarRef{flp, forkVscp, VAccess::WRITE}, "join"};
|
||||
joinp->dtypeSetVoid();
|
||||
addProcessInfo(joinp);
|
||||
addDebugInfo(joinp);
|
||||
AstCAwait* const awaitp = new AstCAwait{flp, joinp};
|
||||
awaitp->dtypeSetVoid();
|
||||
@ -543,13 +570,24 @@ private:
|
||||
m_activep = nullptr;
|
||||
}
|
||||
void visit(AstNodeProcedure* nodep) override {
|
||||
VL_RESTORER(m_procp);
|
||||
m_procp = nodep;
|
||||
iterateChildren(nodep);
|
||||
if (nodep->user2()) nodep->setSuspendable();
|
||||
if (nodep->user2() >= T_SUSP) nodep->setSuspendable();
|
||||
if (nodep->user2() >= T_PROC) nodep->setNeedProcess();
|
||||
}
|
||||
void visit(AstInitial* nodep) override {
|
||||
visit(static_cast<AstNodeProcedure*>(nodep));
|
||||
if (nodep->needProcess() && !nodep->user1SetOnce()) {
|
||||
nodep->addStmtsp(
|
||||
new AstCStmt{nodep->fileline(), "vlProcess->state(VlProcess::FINISHED);\n"});
|
||||
}
|
||||
}
|
||||
void visit(AstAlways* nodep) override {
|
||||
if (nodep->user1SetOnce()) return;
|
||||
iterateChildren(nodep);
|
||||
if (!nodep->user2()) return;
|
||||
if (nodep->user2() == T_PROC) nodep->setNeedProcess();
|
||||
nodep->setSuspendable();
|
||||
FileLine* const flp = nodep->fileline();
|
||||
AstSenTree* const sensesp = m_activep->sensesp();
|
||||
@ -567,6 +605,8 @@ private:
|
||||
m_activep->addNextHere(activep);
|
||||
}
|
||||
void visit(AstCFunc* nodep) override {
|
||||
VL_RESTORER(m_procp);
|
||||
m_procp = nodep;
|
||||
iterateChildren(nodep);
|
||||
if (!nodep->user2()) return;
|
||||
nodep->rtnType("VlCoroutine");
|
||||
@ -588,6 +628,7 @@ private:
|
||||
firstCoStmtp->v3warn(E_UNSUPPORTED,
|
||||
"Unsupported: Timing controls inside DPI-exported tasks");
|
||||
}
|
||||
if (nodep->user2() == T_PROC) nodep->setNeedProcess();
|
||||
}
|
||||
void visit(AstNodeCCall* nodep) override {
|
||||
if (nodep->funcp()->user2() && !nodep->user1SetOnce()) { // If suspendable
|
||||
@ -627,6 +668,7 @@ private:
|
||||
auto* const delayMethodp = new AstCMethodHard{
|
||||
flp, new AstVarRef{flp, getCreateDelayScheduler(), VAccess::WRITE}, "delay", valuep};
|
||||
delayMethodp->dtypeSetVoid();
|
||||
addProcessInfo(delayMethodp);
|
||||
addDebugInfo(delayMethodp);
|
||||
// Create the co_await
|
||||
AstCAwait* const awaitp = new AstCAwait{flp, delayMethodp, getCreateDelaySenTree()};
|
||||
@ -666,6 +708,7 @@ private:
|
||||
flp, new AstVarRef{flp, getCreateDynamicTriggerScheduler(), VAccess::WRITE},
|
||||
"evaluation"};
|
||||
evalMethodp->dtypeSetVoid();
|
||||
addProcessInfo(evalMethodp);
|
||||
auto* const sensesp = nodep->sensesp();
|
||||
addEventDebugInfo(evalMethodp, sensesp);
|
||||
// Create the co_await
|
||||
@ -723,6 +766,7 @@ private:
|
||||
// If it should be committed immediately, pass true, otherwise false
|
||||
triggerMethodp->addPinsp(nodep->user2() ? new AstConst{flp, AstConst::BitTrue{}}
|
||||
: new AstConst{flp, AstConst::BitFalse{}});
|
||||
addProcessInfo(triggerMethodp);
|
||||
addEventDebugInfo(triggerMethodp, sensesp);
|
||||
// Create the co_await
|
||||
AstCAwait* const awaitp = new AstCAwait{flp, triggerMethodp, sensesp};
|
||||
@ -858,6 +902,11 @@ private:
|
||||
}
|
||||
VL_DO_DANGLING(nodep->deleteTree(), nodep);
|
||||
}
|
||||
void visit(AstBegin* nodep) override {
|
||||
VL_RESTORER(m_procp);
|
||||
m_procp = nodep;
|
||||
iterateChildren(nodep);
|
||||
}
|
||||
void visit(AstFork* nodep) override {
|
||||
if (nodep->user1SetOnce()) return;
|
||||
v3Global.setUsesTiming();
|
||||
|
@ -232,7 +232,7 @@ private:
|
||||
const AstWith* m_withp = nullptr; // Current 'with' statement
|
||||
const AstFunc* m_funcp = nullptr; // Current function
|
||||
const AstAttrOf* m_attrp = nullptr; // Current attribute
|
||||
AstNodeModule* m_modp = nullptr; // Current module
|
||||
AstPackage* m_pkgp = nullptr; // Current package
|
||||
const bool m_paramsOnly; // Computing parameter value; limit operation
|
||||
const bool m_doGenerate; // Do errors later inside generate statement
|
||||
int m_dtTables = 0; // Number of created data type tables
|
||||
@ -2628,6 +2628,16 @@ private:
|
||||
}
|
||||
void visit(AstClass* nodep) override {
|
||||
if (nodep->didWidthAndSet()) return;
|
||||
// If the package is std::process, set m_process type to VlProcessRef
|
||||
if (m_pkgp && m_pkgp->name() == "std" && nodep->name() == "process") {
|
||||
if (AstVar* const varp = VN_CAST(nodep->findMember("m_process"), Var)) {
|
||||
AstBasicDType* const dtypep = new AstBasicDType{
|
||||
nodep->fileline(), VBasicDTypeKwd::PROCESS_REFERENCE, VSigning::UNSIGNED};
|
||||
v3Global.rootp()->typeTablep()->addTypesp(dtypep);
|
||||
varp->getChildDTypep()->unlinkFrBack();
|
||||
varp->dtypep(dtypep);
|
||||
}
|
||||
}
|
||||
// Must do extends first, as we may in functions under this class
|
||||
// start following a tree of extends that takes us to other classes
|
||||
VL_RESTORER(m_classp);
|
||||
@ -2636,6 +2646,11 @@ private:
|
||||
userIterateChildren(nodep, nullptr); // First size all members
|
||||
nodep->repairCache();
|
||||
}
|
||||
void visit(AstPackage* nodep) override {
|
||||
VL_RESTORER(m_pkgp);
|
||||
m_pkgp = nodep;
|
||||
userIterateChildren(nodep, nullptr);
|
||||
}
|
||||
void visit(AstThisRef* nodep) override {
|
||||
if (nodep->didWidthAndSet()) return;
|
||||
nodep->dtypep(iterateEditMoveDTypep(nodep, nodep->childDTypep()));
|
||||
@ -2646,12 +2661,6 @@ private:
|
||||
// though causes problems with t_class_forward.v, so for now avoided
|
||||
// userIterateChildren(nodep->classp(), nullptr);
|
||||
}
|
||||
void visit(AstNodeModule* nodep) override {
|
||||
// Visitor does not include AstClass - specialized visitor above
|
||||
VL_RESTORER(m_modp);
|
||||
m_modp = nodep;
|
||||
userIterateChildren(nodep, nullptr);
|
||||
}
|
||||
void visit(AstClassOrPackageRef* nodep) override {
|
||||
if (nodep->didWidthAndSet()) return;
|
||||
userIterateChildren(nodep, nullptr);
|
||||
@ -3491,7 +3500,7 @@ private:
|
||||
VL_DANGLING(index_exprp); // May have been edited
|
||||
return VN_AS(nodep->pinsp(), Arg)->exprp();
|
||||
}
|
||||
void methodCallWarnTiming(AstMethodCall* const nodep, const std::string& className) {
|
||||
void methodCallWarnTiming(AstNodeFTaskRef* const nodep, const std::string& className) {
|
||||
if (v3Global.opt.timing().isSetFalse()) {
|
||||
nodep->v3warn(E_NOTIMING,
|
||||
className << "::" << nodep->name() << "() requires --timing");
|
||||
@ -3515,14 +3524,12 @@ private:
|
||||
if (classp->name() == "semaphore" || classp->name() == "process"
|
||||
|| VString::startsWith(classp->name(), "mailbox")) {
|
||||
// Find the package the class is in
|
||||
AstNode* pkgItemp = classp;
|
||||
while (pkgItemp->backp() && pkgItemp->backp()->nextp() == pkgItemp) {
|
||||
pkgItemp = pkgItemp->backp();
|
||||
}
|
||||
AstPackage* const packagep = VN_CAST(pkgItemp->backp(), Package);
|
||||
AstPackage* const packagep = getItemPackage(classp);
|
||||
// Check if it's std
|
||||
if (packagep && packagep->name() == "std") {
|
||||
if (classp->name() == "semaphore" && nodep->name() == "get") {
|
||||
if (classp->name() == "process") {
|
||||
methodCallWarnTiming(nodep, "process");
|
||||
} else if (classp->name() == "semaphore" && nodep->name() == "get") {
|
||||
methodCallWarnTiming(nodep, "semaphore");
|
||||
} else if (nodep->name() == "put" || nodep->name() == "get"
|
||||
|| nodep->name() == "peek") {
|
||||
@ -5323,6 +5330,7 @@ private:
|
||||
nodep->didWidth(true);
|
||||
return;
|
||||
}
|
||||
if (m_pkgp && m_pkgp->name() == "std") nodep->isFromStd(true);
|
||||
if (nodep->classMethod() && nodep->name() == "rand_mode") {
|
||||
nodep->v3error("The 'rand_mode' method is built-in and cannot be overridden"
|
||||
" (IEEE 1800-2017 18.8)");
|
||||
@ -5405,9 +5413,25 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
AstPackage* getItemPackage(AstNode* pkgItemp) {
|
||||
while (pkgItemp->backp() && pkgItemp->backp()->nextp() == pkgItemp) {
|
||||
pkgItemp = pkgItemp->backp();
|
||||
}
|
||||
return VN_CAST(pkgItemp->backp(), Package);
|
||||
}
|
||||
void visit(AstFuncRef* nodep) override {
|
||||
visit(static_cast<AstNodeFTaskRef*>(nodep));
|
||||
nodep->dtypeFrom(nodep->taskp());
|
||||
if (nodep->fileline()->timingOn()) {
|
||||
AstNodeModule* const classp = nodep->classOrPackagep();
|
||||
if (nodep->name() == "self" && classp->name() == "process") {
|
||||
// Find if package the class is in is std::
|
||||
AstPackage* const packagep = getItemPackage(classp);
|
||||
if (packagep && packagep->name() == "std") {
|
||||
methodCallWarnTiming(nodep, "process");
|
||||
}
|
||||
}
|
||||
}
|
||||
// if (debug()) nodep->dumpTree("- FuncOut: ");
|
||||
}
|
||||
// Returns true if dtypep0 and dtypep1 have same dimensions
|
||||
|
@ -1,3 +1,2 @@
|
||||
[0] %Error: verilated_std.sv:154: Assertion failed in top.std.process.set_randstate: std::process::set_randstate() not supported
|
||||
%Error: verilated_std.sv:154: Verilog $stop
|
||||
Aborting...
|
||||
'{m_process:process}
|
||||
*-* All Finished *-*
|
||||
|
@ -10,14 +10,19 @@ if (!$::Driver) { use FindBin; exec("$FindBin::Bin/bootstrap.pl", @ARGV, $0); di
|
||||
|
||||
scenarios(simulator => 1);
|
||||
|
||||
compile(
|
||||
);
|
||||
if (!$Self->have_coroutines) {
|
||||
skip("No coroutine support");
|
||||
}
|
||||
else {
|
||||
compile(
|
||||
v_flags2 => ["--timing"],
|
||||
);
|
||||
|
||||
execute(
|
||||
fails => $Self->{vlt_all},
|
||||
expect_filename => $Self->{golden_filename},
|
||||
check_finished => !$Self->{vlt_all},
|
||||
);
|
||||
execute(
|
||||
check_finished => 1,
|
||||
expect_filename => $Self->{golden_filename},
|
||||
);
|
||||
}
|
||||
|
||||
ok(1);
|
||||
1;
|
||||
|
@ -38,6 +38,8 @@ module t(/*AUTOARG*/);
|
||||
p.srandom(0);
|
||||
p.set_randstate(p.get_randstate());
|
||||
|
||||
$display("%p", p);
|
||||
|
||||
$write("*-* All Finished *-*\n");
|
||||
$finish;
|
||||
end
|
||||
|
@ -11,7 +11,7 @@ if (!$::Driver) { use FindBin; exec("$FindBin::Bin/bootstrap.pl", @ARGV, $0); di
|
||||
scenarios(vlt => 1);
|
||||
|
||||
lint(
|
||||
verilator_flags2 => ["--xml-only"],
|
||||
verilator_flags2 => ["--xml-only", "--timing"],
|
||||
fails => 1,
|
||||
expect_filename => $Self->{golden_filename},
|
||||
);
|
||||
|
27
test_regress/t/t_process_finished.pl
Executable file
27
test_regress/t/t_process_finished.pl
Executable file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env perl
|
||||
if (!$::Driver) { use FindBin; exec("$FindBin::Bin/bootstrap.pl", @ARGV, $0); die; }
|
||||
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
|
||||
#
|
||||
# Copyright 2023 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
|
||||
|
||||
scenarios(simulator => 1);
|
||||
|
||||
if (!$Self->have_coroutines) {
|
||||
skip("No coroutine support");
|
||||
}
|
||||
else {
|
||||
compile(
|
||||
verilator_flags2 => ["--timing"],
|
||||
);
|
||||
|
||||
execute(
|
||||
check_finished => 1,
|
||||
);
|
||||
}
|
||||
|
||||
ok(1);
|
||||
1;
|
24
test_regress/t/t_process_finished.v
Normal file
24
test_regress/t/t_process_finished.v
Normal file
@ -0,0 +1,24 @@
|
||||
// DESCRIPTION: Verilator: Verilog Test module
|
||||
//
|
||||
// This file ONLY is placed under the Creative Commons Public Domain, for
|
||||
// any use, without warranty, 2023 by Antmicro Ltd.
|
||||
// SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
module t (/*AUTOARG*/
|
||||
// Inputs
|
||||
clk
|
||||
);
|
||||
input clk;
|
||||
process p;
|
||||
|
||||
initial begin
|
||||
p = process::self();
|
||||
end
|
||||
|
||||
always @(posedge clk) begin
|
||||
if (p.status() != process::FINISHED)
|
||||
$stop;
|
||||
$write("*-* All Finished *-*\n");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
11
test_regress/t/t_process_fork.out
Normal file
11
test_regress/t/t_process_fork.out
Normal file
@ -0,0 +1,11 @@
|
||||
job started
|
||||
job started
|
||||
job started
|
||||
job started
|
||||
job started
|
||||
job started
|
||||
job started
|
||||
job started
|
||||
all jobs started
|
||||
all jobs finished
|
||||
*-* All Finished *-*
|
28
test_regress/t/t_process_fork.pl
Executable file
28
test_regress/t/t_process_fork.pl
Executable file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env perl
|
||||
if (!$::Driver) { use FindBin; exec("$FindBin::Bin/bootstrap.pl", @ARGV, $0); die; }
|
||||
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
|
||||
#
|
||||
# Copyright 2023 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
|
||||
|
||||
scenarios(simulator => 1);
|
||||
|
||||
if (!$Self->have_coroutines) {
|
||||
skip("No coroutine support");
|
||||
}
|
||||
else {
|
||||
compile(
|
||||
verilator_flags2 => ["--timing"],
|
||||
);
|
||||
|
||||
execute(
|
||||
check_finished => 1,
|
||||
expect_filename => $Self->{golden_filename},
|
||||
);
|
||||
}
|
||||
|
||||
ok(1);
|
||||
1;
|
30
test_regress/t/t_process_fork.v
Normal file
30
test_regress/t/t_process_fork.v
Normal file
@ -0,0 +1,30 @@
|
||||
// DESCRIPTION: Verilator: Verilog Test module
|
||||
//
|
||||
// This file ONLY is placed under the Creative Commons Public Domain, for
|
||||
// any use, without warranty, 2023 by Antmicro Ltd.
|
||||
// SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
module t;
|
||||
process job[] = new [8];
|
||||
bit is_alloc = 0;
|
||||
|
||||
initial begin
|
||||
foreach (job[j]) fork
|
||||
begin
|
||||
$write("job started\n");
|
||||
job[j] = process::self();
|
||||
end
|
||||
join_none
|
||||
foreach (job[j]) begin
|
||||
is_alloc = !!job[j];
|
||||
wait (is_alloc);
|
||||
end
|
||||
$write("all jobs started\n");
|
||||
foreach (job[j]) begin
|
||||
job[j].await();
|
||||
end
|
||||
$write("all jobs finished\n");
|
||||
$write("*-* All Finished *-*\n");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
27
test_regress/t/t_process_kill.pl
Executable file
27
test_regress/t/t_process_kill.pl
Executable file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env perl
|
||||
if (!$::Driver) { use FindBin; exec("$FindBin::Bin/bootstrap.pl", @ARGV, $0); die; }
|
||||
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
|
||||
#
|
||||
# Copyright 2023 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
|
||||
|
||||
scenarios(simulator => 1);
|
||||
|
||||
if (!$Self->have_coroutines) {
|
||||
skip("No coroutine support");
|
||||
}
|
||||
else {
|
||||
compile(
|
||||
verilator_flags2 => ["--timing"],
|
||||
);
|
||||
|
||||
execute(
|
||||
check_finished => 1,
|
||||
);
|
||||
}
|
||||
|
||||
ok(1);
|
||||
1;
|
31
test_regress/t/t_process_kill.v
Normal file
31
test_regress/t/t_process_kill.v
Normal file
@ -0,0 +1,31 @@
|
||||
// DESCRIPTION: Verilator: Verilog Test module
|
||||
//
|
||||
// This file ONLY is placed under the Creative Commons Public Domain, for
|
||||
// any use, without warranty, 2023 by Antmicro Ltd.
|
||||
// SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
module t (/*AUTOARG*/
|
||||
// Inputs
|
||||
clk
|
||||
);
|
||||
input clk;
|
||||
process p;
|
||||
bit s = 0;
|
||||
|
||||
initial begin
|
||||
wait (s);
|
||||
p.kill();
|
||||
p.await();
|
||||
$write("*-* All Finished *-*\n");
|
||||
$finish;
|
||||
end
|
||||
|
||||
always @(posedge clk) begin
|
||||
if (!p) begin
|
||||
p = process::self();
|
||||
s = 1;
|
||||
end else begin
|
||||
$stop;
|
||||
end
|
||||
end
|
||||
endmodule
|
54
test_regress/t/t_process_notiming.out
Normal file
54
test_regress/t/t_process_notiming.out
Normal file
@ -0,0 +1,54 @@
|
||||
%Error-NOTIMING: t/t_process.v:26:20: process::self() requires --timing
|
||||
: ... In instance t
|
||||
26 | p = process::self();
|
||||
| ^~~~
|
||||
... For error description see https://verilator.org/warn/NOTIMING?v=latest
|
||||
%Error-NOTIMING: t/t_process.v:27:13: process::status() requires --timing
|
||||
: ... In instance t
|
||||
27 | if (p.status() != process::RUNNING) $stop;
|
||||
| ^~~~~~
|
||||
%Error-NOTIMING: t/t_process.v:28:13: process::status() requires --timing
|
||||
: ... In instance t
|
||||
28 | if (p.status() == process::WAITING) $stop;
|
||||
| ^~~~~~
|
||||
%Error-NOTIMING: t/t_process.v:29:13: process::status() requires --timing
|
||||
: ... In instance t
|
||||
29 | if (p.status() == process::SUSPENDED) $stop;
|
||||
| ^~~~~~
|
||||
%Error-NOTIMING: t/t_process.v:30:13: process::status() requires --timing
|
||||
: ... In instance t
|
||||
30 | if (p.status() == process::KILLED) $stop;
|
||||
| ^~~~~~
|
||||
%Error-NOTIMING: t/t_process.v:31:13: process::status() requires --timing
|
||||
: ... In instance t
|
||||
31 | if (p.status() == process::FINISHED) $stop;
|
||||
| ^~~~~~
|
||||
%Error-NOTIMING: t/t_process.v:33:16: process::kill() requires --timing
|
||||
: ... In instance t
|
||||
33 | if (0) p.kill();
|
||||
| ^~~~
|
||||
%Error-NOTIMING: t/t_process.v:34:16: process::await() requires --timing
|
||||
: ... In instance t
|
||||
34 | if (0) p.await();
|
||||
| ^~~~~
|
||||
%Error-NOTIMING: t/t_process.v:35:16: process::suspend() requires --timing
|
||||
: ... In instance t
|
||||
35 | if (0) p.suspend();
|
||||
| ^~~~~~~
|
||||
%Error-NOTIMING: t/t_process.v:36:16: process::resume() requires --timing
|
||||
: ... In instance t
|
||||
36 | if (0) p.resume();
|
||||
| ^~~~~~
|
||||
%Error-NOTIMING: t/t_process.v:38:9: process::srandom() requires --timing
|
||||
: ... In instance t
|
||||
38 | p.srandom(0);
|
||||
| ^~~~~~~
|
||||
%Error-NOTIMING: t/t_process.v:39:25: process::get_randstate() requires --timing
|
||||
: ... In instance t
|
||||
39 | p.set_randstate(p.get_randstate());
|
||||
| ^~~~~~~~~~~~~
|
||||
%Error-NOTIMING: t/t_process.v:39:9: process::set_randstate() requires --timing
|
||||
: ... In instance t
|
||||
39 | p.set_randstate(p.get_randstate());
|
||||
| ^~~~~~~~~~~~~
|
||||
%Error: Exiting due to
|
22
test_regress/t/t_process_notiming.pl
Executable file
22
test_regress/t/t_process_notiming.pl
Executable file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env perl
|
||||
if (!$::Driver) { use FindBin; exec("$FindBin::Bin/bootstrap.pl", @ARGV, $0); die; }
|
||||
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
|
||||
#
|
||||
# Copyright 2020 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
|
||||
|
||||
scenarios(simulator => 1);
|
||||
|
||||
top_filename("t/t_process.v");
|
||||
|
||||
compile(
|
||||
expect_filename => $Self->{golden_filename},
|
||||
v_flags2 => ["+define+T_PROCESS+std::process", "--no-timing"],
|
||||
fails => 1,
|
||||
);
|
||||
|
||||
ok(1);
|
||||
1;
|
27
test_regress/t/t_process_rand.pl
Executable file
27
test_regress/t/t_process_rand.pl
Executable file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env perl
|
||||
if (!$::Driver) { use FindBin; exec("$FindBin::Bin/bootstrap.pl", @ARGV, $0); die; }
|
||||
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
|
||||
#
|
||||
# Copyright 2003 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
|
||||
|
||||
scenarios(simulator => 1);
|
||||
|
||||
if (!$Self->have_coroutines) {
|
||||
skip("No coroutine support");
|
||||
}
|
||||
else {
|
||||
compile(
|
||||
v_flags2 => ["--timing"],
|
||||
);
|
||||
|
||||
execute(
|
||||
check_finished => 1,
|
||||
);
|
||||
}
|
||||
|
||||
ok(1);
|
||||
1;
|
52
test_regress/t/t_process_rand.v
Normal file
52
test_regress/t/t_process_rand.v
Normal file
@ -0,0 +1,52 @@
|
||||
// DESCRIPTION: Verilator: Verilog Test module
|
||||
//
|
||||
// This file ONLY is placed under the Creative Commons Public Domain, for
|
||||
// any use, without warranty, 2023 by Antmicro Ltd.
|
||||
// SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
module t;
|
||||
process p;
|
||||
|
||||
integer seed;
|
||||
string state;
|
||||
int a;
|
||||
int b;
|
||||
|
||||
initial begin
|
||||
p = process::self();
|
||||
|
||||
// Test setting RNG state with state string
|
||||
state = p.get_randstate();
|
||||
p.set_randstate(state);
|
||||
a = $random;
|
||||
p.set_randstate(state);
|
||||
b = $random;
|
||||
$display("a=%d, b=%d", a, b);
|
||||
if (a != b) $stop;
|
||||
|
||||
// Test the same with $urandom
|
||||
state = p.get_randstate();
|
||||
p.set_randstate(state);
|
||||
a = $urandom;
|
||||
p.set_randstate(state);
|
||||
b = $urandom;
|
||||
$display("a=%d, b=%d", a, b);
|
||||
if (a != b) $stop;
|
||||
|
||||
// Test if the results repeat after the state is reset
|
||||
state = p.get_randstate();
|
||||
for (int i = 0; i < 10; i++)
|
||||
$random;
|
||||
a = $random;
|
||||
// Now reset the state and take 11th result again
|
||||
p.set_randstate(state);
|
||||
for (int i = 0; i < 10; i++)
|
||||
$random;
|
||||
b = $random;
|
||||
$display("a=%d, b=%d", a, b);
|
||||
if (a != b) $stop;
|
||||
|
||||
$write("*-* All Finished *-*\n");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
@ -12,15 +12,18 @@ scenarios(simulator => 1);
|
||||
|
||||
top_filename("t/t_process.v");
|
||||
|
||||
compile(
|
||||
v_flags2 => ["+define+T_PROCESS+std::process"],
|
||||
);
|
||||
if (!$Self->have_coroutines) {
|
||||
skip("No coroutine support");
|
||||
}
|
||||
else {
|
||||
compile(
|
||||
v_flags2 => ["+define+T_PROCESS+std::process", "--timing"],
|
||||
);
|
||||
|
||||
execute(
|
||||
check_finished => !$Self->{vlt_all},
|
||||
fails => $Self->{vlt_all},
|
||||
expect_filename => $Self->{golden_filename},
|
||||
);
|
||||
execute(
|
||||
check_finished => 1,
|
||||
) if !$Self->{vlt_all};
|
||||
}
|
||||
|
||||
ok(1);
|
||||
1;
|
||||
|
Loading…
Reference in New Issue
Block a user