diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 487980e40..6ef897b2f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -189,6 +189,7 @@ set(HEADERS V3Tristate.h V3Udp.h V3Undriven.h + V3UndrivenCapture.h V3UniqueNames.h V3Unknown.h V3Unroll.h @@ -358,6 +359,7 @@ set(COMMON_SOURCES V3Tristate.cpp V3Udp.cpp V3Undriven.cpp + V3UndrivenCapture.cpp V3Unknown.cpp V3Unroll.cpp V3UnrollGen.cpp diff --git a/src/Makefile_obj.in b/src/Makefile_obj.in index 1bc6cc3af..77f951678 100644 --- a/src/Makefile_obj.in +++ b/src/Makefile_obj.in @@ -341,6 +341,7 @@ RAW_OBJS_PCH_ASTNOMT = \ V3Tristate.o \ V3Udp.o \ V3Undriven.o \ + V3UndrivenCapture.o \ V3Unknown.o \ V3Unroll.o \ V3UnrollGen.o \ diff --git a/src/V3Undriven.cpp b/src/V3Undriven.cpp index 75e646520..e3e986e8b 100644 --- a/src/V3Undriven.cpp +++ b/src/V3Undriven.cpp @@ -28,6 +28,7 @@ #include "V3Undriven.h" #include "V3Stats.h" +#include "V3UndrivenCapture.h" #include @@ -51,6 +52,9 @@ class UndrivenVarEntry final { const FileLine* m_nodeFileLinep = nullptr; // File line of varref if driven, else nullptr bool m_underGen = false; // Under a generate + const AstNodeFTaskRef* m_callNodep + = nullptr; // Call node if driven via writeSummary, else nullptr + enum : uint8_t { FLAG_USED = 0, FLAG_DRIVEN = 1, FLAG_DRIVEN_ALWCOMB = 2, FLAGS_PER_BIT = 3 }; public: @@ -278,6 +282,12 @@ public: } } } + + void drivenViaCall(const AstNodeFTaskRef* nodep) { + drivenWhole(); + if (!m_callNodep) { m_callNodep = nodep; } + } + const AstNodeFTaskRef* callNodep() const { return m_callNodep; } }; //###################################################################### @@ -304,6 +314,8 @@ class UndrivenVisitor final : public VNVisitorConst { const AstAlways* m_alwaysp = nullptr; // Current always of either type const AstAlways* m_alwaysCombp = nullptr; // Current always if combo, otherwise nullptr + V3UndrivenCapture* const m_capturep = nullptr; // Capture object. 'nullptr' if disabled. + // METHODS UndrivenVarEntry* getEntryp(AstVar* nodep, int which_user) { @@ -420,6 +432,16 @@ class UndrivenVisitor final : public VNVisitorConst { << " (IEEE 1800-2023 13.5): " << nodep->prettyNameQ()); } } + + // If writeSummary is enabled, task/function definitions are treated as non-executed. + // Their effects are applied at call sites via writeSummary(), so don't let definition + // traversal create phantom "other writes" for MULTIDRIVEN. + if (m_taskp && !m_alwaysp && !m_inContAssign && !m_inInitialStatic && !m_inBBox + && !m_taskp->dpiExport()) { + AstVar* const retVarp = VN_CAST(m_taskp->fvarp(), Var); + if (!retVarp || nodep->varp() != retVarp) return; + } + for (int usr = 1; usr < (m_alwaysCombp ? 3 : 2); ++usr) { UndrivenVarEntry* const entryp = getEntryp(nodep->varp(), usr); const bool fdrv = nodep->access().isWriteOrRW() @@ -432,7 +454,12 @@ class UndrivenVisitor final : public VNVisitorConst { if (entryp->isDrivenWhole() && !m_inBBox && !VN_IS(nodep, VarXRef) && !VN_IS(nodep->dtypep()->skipRefp(), UnpackArrayDType) && nodep->fileline() != entryp->getNodeFileLinep() && !entryp->isUnderGen() - && entryp->getNodep()) { + && (entryp->getNodep() || entryp->callNodep())) { + + const AstNode* const otherWritep + = entryp->getNodep() ? static_cast(entryp->getNodep()) + : entryp->callNodep(); + if (m_alwaysCombp && (!entryp->isDrivenAlwaysCombWhole() || (m_alwaysCombp != entryp->getAlwCombp() @@ -443,20 +470,18 @@ class UndrivenVisitor final : public VNVisitorConst { << " (IEEE 1800-2023 9.2.2.2): " << nodep->prettyNameQ() << '\n' << nodep->warnOther() << '\n' << nodep->warnContextPrimary() << '\n' - << entryp->getNodep()->warnOther() - << "... Location of other write\n" - << entryp->getNodep()->warnContextSecondary()); + << otherWritep->warnOther() << "... Location of other write\n" + << otherWritep->warnContextSecondary()); } if (!m_alwaysCombp && entryp->isDrivenAlwaysCombWhole()) { - nodep->v3warn(MULTIDRIVEN, - "Variable also written to in always_comb" - << " (IEEE 1800-2023 9.2.2.2): " << nodep->prettyNameQ() - << '\n' - << nodep->warnOther() << '\n' - << nodep->warnContextPrimary() << '\n' - << entryp->getNodep()->warnOther() - << "... Location of always_comb write\n" - << entryp->getNodep()->warnContextSecondary()); + nodep->v3warn(MULTIDRIVEN, "Variable also written to in always_comb" + << " (IEEE 1800-2023 9.2.2.2): " + << nodep->prettyNameQ() << '\n' + << nodep->warnOther() << '\n' + << nodep->warnContextPrimary() << '\n' + << otherWritep->warnOther() + << "... Location of always_comb write\n" + << otherWritep->warnContextSecondary()); } } entryp->drivenWhole(nodep, nodep->fileline()); @@ -523,10 +548,39 @@ class UndrivenVisitor final : public VNVisitorConst { iterateChildrenConst(nodep); if (nodep->keyword() == VAlwaysKwd::ALWAYS_COMB) UINFO(9, " Done " << nodep); } + void visit(AstNodeFTaskRef* nodep) override { VL_RESTORER(m_inFTaskRef); m_inFTaskRef = true; + iterateChildrenConst(nodep); + + if (!m_capturep) return; + + // If writeSummary is enabled, task/function definitions are treated as non-executed. + // Do not apply writeSummary at calls inside a task definition, or they will look like + // independent drivers (phantom MULTIDRIVEN). Did the lambda on purpose - lessen chance of + // screwup in future edits. + + const auto inExecutedContext = [this]() { + return !(m_taskp && !m_alwaysp && !m_inContAssign && !m_inInitialStatic && !m_inBBox + && !m_taskp->dpiExport()); + }; + + if (!inExecutedContext()) return; + + AstNodeFTask* const calleep = nodep->taskp(); + if (!calleep) return; + + const auto& vars = m_capturep->writeSummary(calleep); + for (AstVar* const varp : vars) { + for (int usr = 1; usr < (m_alwaysCombp ? 3 : 2); ++usr) { + UndrivenVarEntry* const entryp = getEntryp(varp, usr); + entryp->drivenViaCall(nodep); + if (m_alwaysCombp) + entryp->drivenAlwaysCombWhole(m_alwaysCombp, m_alwaysCombp->fileline()); + } + } } void visit(AstNodeFTask* nodep) override { @@ -556,7 +610,11 @@ class UndrivenVisitor final : public VNVisitorConst { public: // CONSTRUCTORS - explicit UndrivenVisitor(AstNetlist* nodep) { iterateConst(nodep); } + explicit UndrivenVisitor(AstNetlist* nodep, V3UndrivenCapture* capturep) + : m_capturep{capturep} { + iterateConst(nodep); + } + ~UndrivenVisitor() override { for (UndrivenVarEntry* ip : m_entryps[1]) ip->reportViolations(); for (int usr = 1; usr < 3; ++usr) { @@ -570,6 +628,9 @@ public: void V3Undriven::undrivenAll(AstNetlist* nodep) { UINFO(2, __FUNCTION__ << ":"); - { UndrivenVisitor{nodep}; } + + V3UndrivenCapture capture{nodep}; + UndrivenVisitor{nodep, &capture}; + if (v3Global.opt.stats()) V3Stats::statsStage("undriven"); } diff --git a/src/V3UndrivenCapture.cpp b/src/V3UndrivenCapture.cpp new file mode 100644 index 000000000..4aa1ff06a --- /dev/null +++ b/src/V3UndrivenCapture.cpp @@ -0,0 +1,212 @@ +// -*- mode: C++; c-file-style: "cc-mode" -*- +//************************************************************************* +// DESCRIPTION: Verilator: Capture task/function write summaries for undriven checks +// +// Code available from: https://verilator.org +// +//************************************************************************* +// +// Copyright 2003-2025 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 +// +//************************************************************************* + +#include "V3UndrivenCapture.h" + +#include "V3Error.h" +#include "V3Global.h" + +VL_DEFINE_DEBUG_FUNCTIONS; + +namespace { + +struct Stats final { + uint64_t ftasks = 0; + uint64_t varWrites = 0; + uint64_t callEdges = 0; +} g_stats; + +static std::string taskNameQ(const AstNodeFTask* taskp) { + if (!taskp) return ""; + return taskp->prettyNameQ(); +} + +class CaptureVisitor final : public VNVisitorConst { + V3UndrivenCapture& m_cap; + const AstNodeFTask* m_curTaskp = nullptr; + + static void iterateListConst(VNVisitorConst& v, AstNode* nodep) { + for (AstNode* np = nodep; np; np = np->nextp()) np->accept(v); + } + +public: + explicit CaptureVisitor(V3UndrivenCapture& cap, AstNetlist* netlistp) + : m_cap{cap} { + iterateConst(netlistp); + } + +private: + // Visit a task/function definition and collect direct writes and direct callees. + void visit(AstNodeFTask* nodep) override { + VL_RESTORER(m_curTaskp); + m_curTaskp = nodep; + ++g_stats.ftasks; + UINFO(9, "undriven capture enter ftask " << nodep << " " << nodep->prettyNameQ()); + m_cap.noteTask(nodep); + iterateListConst(*this, nodep->stmtsp()); + } + + void visit(AstNodeVarRef* nodep) override { + if (m_curTaskp && nodep->access().isWriteOrRW()) { + ++g_stats.varWrites; + UINFO(9, "undriven capture direct write in " << taskNameQ(m_curTaskp) + << " var=" << nodep->varp()->prettyNameQ() + << " at " << nodep->fileline()); + + m_cap.noteDirectWrite(m_curTaskp, nodep->varp()); + } + iterateChildrenConst(nodep); + } + + void visit(AstNodeFTaskRef* nodep) override { + // Record the call edge if resolved + if (m_curTaskp) { + if (AstNodeFTask* const calleep = nodep->taskp()) { + ++g_stats.callEdges; + UINFO(9, "undriven capture call edge " << taskNameQ(m_curTaskp) << " -> " + << taskNameQ(calleep)); + m_cap.noteCallEdge(m_curTaskp, calleep); + } else { + UINFO(9, "undriven capture unresolved call in " << taskNameQ(m_curTaskp) + << " name=" << nodep->name()); + } + } + iterateChildrenConst(nodep); // still scan pins/args + } + + void visit(AstNode* nodep) override { iterateChildrenConst(nodep); } +}; + +} // namespace + +V3UndrivenCapture::V3UndrivenCapture(AstNetlist* netlistp) { + gather(netlistp); + + // Compute summaries for all tasks + for (const auto& kv : m_info) (void)computeWriteSummary(kv.first); + + // Release the filter memory + for (auto& kv : m_info) { + kv.second.calleesSet.clear(); + kv.second.calleesSet.rehash(0); + kv.second.directWritesSet.clear(); + kv.second.directWritesSet.rehash(0); + } + + UINFO(9, "undriven capture stats ftasks=" + << g_stats.ftasks << " varWrites=" << g_stats.varWrites + << " callEdges=" << g_stats.callEdges << " uniqueTasks=" << m_info.size()); +} + +void V3UndrivenCapture::gather(AstNetlist* netlistp) { + // Walk netlist and populate m_info with direct writes + call edges + CaptureVisitor{*this, netlistp}; +} + +const V3UndrivenCapture::FTaskInfo* V3UndrivenCapture::find(const AstNodeFTask* taskp) const { + const auto it = m_info.find(taskp); + if (it == m_info.end()) return nullptr; + return &it->second; +} + +const std::vector& V3UndrivenCapture::writeSummary(const AstNodeFTask* taskp) { + // Ensure entry exists even if empty + (void)m_info[taskp]; + return computeWriteSummary(taskp); +} + +const std::vector& V3UndrivenCapture::computeWriteSummary(const AstNodeFTask* taskp) { + FTaskInfo& info = m_info[taskp]; + + if (info.state == State::DONE) { + UINFO(9, "undriven capture writeSummary cached size=" << info.writeSummary.size() + << " for " << taskNameQ(taskp)); + return info.writeSummary; + } + if (info.state == State::VISITING) { + UINFO(9, "undriven capture recursion detected at " + << taskNameQ(taskp) + << " returning directWrites size=" << info.directWrites.size()); + // Cycle detected. return directWrites only to guarantee termination. + if (info.writeSummary.empty()) info.writeSummary = info.directWrites; + return info.writeSummary; + } + + info.state = State::VISITING; + + info.writeSummary.clear(); + + // Prevent duplicates across all sources that can contribute to a write summary (direct writes + // and call chains) + std::unordered_set seen; + + // Simple lambda for filtering duplicates + auto addVar = [&](AstVar* v) { + if (seen.insert(v).second) info.writeSummary.push_back(v); + }; + + // Start with direct writes + for (AstVar* v : info.directWrites) addVar(v); + + // Add callee summaries + for (const AstNodeFTask* calleep : info.callees) { + if (m_info.find(calleep) == m_info.end()) continue; + const std::vector& sub = computeWriteSummary(calleep); + for (AstVar* v : sub) addVar(v); + } + + UINFO(9, "undriven capture writeSummary computed size=" << info.writeSummary.size() << " for " + << taskNameQ(taskp)); + + // We are done, so set the m_info state correctly and return the vector of variables + info.state = State::DONE; + return info.writeSummary; +} + +void V3UndrivenCapture::noteTask(const AstNodeFTask* taskp) { (void)m_info[taskp]; } + +void V3UndrivenCapture::noteDirectWrite(const AstNodeFTask* taskp, AstVar* varp) { + FTaskInfo& info = m_info[taskp]; + + // Exclude function return variable (not an externally visible side-effect) + AstVar* const retVarp = VN_CAST(taskp->fvarp(), Var); + if (retVarp && varp == retVarp) return; + + // Filter out duplicates. + if (info.directWritesSet.insert(varp).second) { info.directWrites.push_back(varp); } +} + +void V3UndrivenCapture::noteCallEdge(const AstNodeFTask* callerp, const AstNodeFTask* calleep) { + FTaskInfo& callerInfo = m_info[callerp]; + // Prevents duplicate entries from being appended, if calleep already exists then insert will + // return false, and then is not inserted into the callees vector. + if (callerInfo.calleesSet.insert(calleep).second) { callerInfo.callees.push_back(calleep); } + // Ensure callee entry exists, if already exists then this is a no-op. unordered_map<> so + // cheap. + (void)m_info[calleep]; +} + +void V3UndrivenCapture::debugDumpTask(const AstNodeFTask* taskp, int level) const { + const auto* const infop = find(taskp); + if (!infop) { + UINFO(level, "undriven capture no entry for task " << taskp); + return; + } + UINFO(level, "undriven capture dump task " << taskp << " " << taskp->prettyNameQ() + << " directWrites=" << infop->directWrites.size() + << " callees=" << infop->callees.size() + << " writeSummary=" << infop->writeSummary.size()); +} diff --git a/src/V3UndrivenCapture.h b/src/V3UndrivenCapture.h new file mode 100644 index 000000000..526e6843c --- /dev/null +++ b/src/V3UndrivenCapture.h @@ -0,0 +1,108 @@ +// -*- mode: C++; c-file-style: "cc-mode" -*- +//************************************************************************* +// DESCRIPTION: Verilator: Capture task/function write summaries for undriven checks +// +// Code available from: https://verilator.org +// +//************************************************************************* +// +// Copyright 2003-2025 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 +// +//************************************************************************* + +//************************************************************************* +// +// Capture task/function write summaries for multidriven checks. +// Per-task/function capture info keyed by resolved AstNodeFTask* identity (FTask = function or +// task). This is our 'graph' of tasks/functions. Each node has a list of direct callees and +// a list of variables written in the function body. There are methods to dedup after walking the +// tree. V3Undriven then uses the writeSummary for multidriven checks - i.e. it treats writes (side +// effects) inside subroutines as part of the caller's process. +// +//************************************************************************* + +#ifndef VERILATOR_V3UNDRIVENCAPTURE_H_ +#define VERILATOR_V3UNDRIVENCAPTURE_H_ + +#include "config_build.h" + +#include "V3Ast.h" + +#include +#include +#include + +class AstNetlist; + +class V3UndrivenCapture final { +public: + // DFS computation state for writeSummary propagation. + // UNVISITED: write summary not computed yet + // VISITING: currently computing on the call stack - used to detect cycles + // DONE: write summary computed + enum class State : uint8_t { UNVISITED, VISITING, DONE }; + + struct FTaskInfo final { + // Variables written directly in this task/function body (iteration order) + std::vector directWrites; + // Direct resolved callees from this task/function body (iteration order) + std::vector callees; + // 'Write through write' writeSummary for the given task/function. Meaning ultimately + // everything that this function/task writes to. + std::vector writeSummary; + // State for writeSummary computation. + State state = State::UNVISITED; + // This is used to test whether weve already recorded a callee. Used to 'filter' on insert + // versus sorting at the end. + std::unordered_set calleesSet; + // This is used to test whether weve already recorded a direct write. Used to 'filter' on + // insert versus sorting at the end. + std::unordered_set directWritesSet; + }; + +private: + // Per-task/function capture info keyed by resolved AstNodeFTask* identity (FTask = function or + // task). This is our 'graph' of tasks/functions. Each node has a list of direct callees and + // a list of variables written in the function body. There are methods to remove duplicates + // otherwise this could explode. + std::unordered_map m_info; + + // Collect direct writes and call edges for all tasks/functions. Run one time when + // UndrivenCapture is created. This runs the visitor over the tree. + void gather(AstNetlist* netlistp); + // Compute (and cache) 'write through write' writeSummary for the given task/function. + const std::vector& computeWriteSummary(const AstNodeFTask* taskp); + +public: + // Build capture database and precompute writeSummary for all discovered tasks/functions. + explicit V3UndrivenCapture(AstNetlist* netlistp); + + // Lookup task/function capture info (nullptr if unknown). This is currently only used for the + // debug helper. + const FTaskInfo* find(const AstNodeFTask* taskp) const; + // Get write through write through write, etc (call chain) writeSummary for a task/function + // (creates empty entry if needed). This returns a vector of variables that a particular + // task/function writes to, including all variables written by functions called by this + // task/function, and so on. + const std::vector& writeSummary(const AstNodeFTask* taskp); + + // Used by the capture visitor to record information about tasks/functions and their statements + // and callees. noteTask() makes sure there is an entry for the given taskp. + void noteTask(const AstNodeFTask* taskp); + // Inside the body of taskp there is a write to variable varp + void noteDirectWrite(const AstNodeFTask* taskp, AstVar* varp); + // Inside the body of callerp there is a call to calleep, this is needed so we can create a + // summary that includes all variables written by functions called by this task/function, and + // so on. + void noteCallEdge(const AstNodeFTask* callerp, const AstNodeFTask* calleep); + + // Dump one task's summary for debugging. leaving this in, in case need to debug future + // functionality. + void debugDumpTask(const AstNodeFTask* taskp, int level = 9) const; +}; + +#endif // VERILATOR_V3UNDRIVENCAPTURE_H_ diff --git a/test_regress/t/t_lint_multidriven_taskcall_bad.out b/test_regress/t/t_lint_multidriven_taskcall_bad.out new file mode 100644 index 000000000..1d1278b14 --- /dev/null +++ b/test_regress/t/t_lint_multidriven_taskcall_bad.out @@ -0,0 +1,11 @@ +%Warning-MULTIDRIVEN: t/t_lint_multidriven_taskcall_bad.v:28:15: Variable written to in always_comb also written by other process (IEEE 1800-2023 9.2.2.2): 'out' + : ... note: In instance 't' + t/t_lint_multidriven_taskcall_bad.v:28:15: + 28 | if (sel2) out = 1'b1; + | ^~~ + t/t_lint_multidriven_taskcall_bad.v:20:5: ... Location of other write + 20 | out = 1'b0; + | ^~~ + ... For warning description see https://verilator.org/warn/MULTIDRIVEN?v=latest + ... Use "/* verilator lint_off MULTIDRIVEN */" and lint_on around source to disable this message. +%Error: Exiting due to diff --git a/test_regress/t/t_lint_multidriven_taskcall_bad.py b/test_regress/t/t_lint_multidriven_taskcall_bad.py new file mode 100755 index 000000000..31228c9a7 --- /dev/null +++ b/test_regress/t/t_lint_multidriven_taskcall_bad.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# Copyright 2024 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('linter') + +test.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_lint_multidriven_taskcall_bad.v b/test_regress/t/t_lint_multidriven_taskcall_bad.v new file mode 100644 index 000000000..b7943087a --- /dev/null +++ b/test_regress/t/t_lint_multidriven_taskcall_bad.v @@ -0,0 +1,32 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed into the Public Domain, for any use, +// without warranty. +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input logic sel, + input logic sel2, + input logic d, + output logic out +); + + task automatic do_stuff(input logic din); + out = din; + endtask + + // Driver #1 (via task call) + always_comb begin + out = 1'b0; + if (sel) do_stuff(d); + end + + // Driver #2 (separate process) + // I only want the MULTIDRIVEN. +/* verilator lint_off LATCH */ + always_comb begin + if (sel2) out = 1'b1; + end +/* verilator lint_on LATCH */ + +endmodule diff --git a/test_regress/t/t_multidriven_class.py b/test_regress/t/t_multidriven_class.py new file mode 100755 index 000000000..c6e56559a --- /dev/null +++ b/test_regress/t/t_multidriven_class.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# Copyright 2025 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() diff --git a/test_regress/t/t_multidriven_class.v b/test_regress/t/t_multidriven_class.v new file mode 100644 index 000000000..9e93c55b6 --- /dev/null +++ b/test_regress/t/t_multidriven_class.v @@ -0,0 +1,284 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed into the Public Domain, for any use, +// without warranty. +// SPDX-License-Identifier: CC0-1.0 + +// Consolidated class-based task/function multidriven tests +// (formerly t_multidriven_class{0,1,2,3,4,f0,f1}.v) + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d (%s !== %s)\n", `__FILE__,`__LINE__, (gotv), (expv), `"gotv`", `"expv`"); `stop; end while(0); +// verilog_format: on + +//---------------------------------------------------------------------- +// class0: class task writes through ref argument (direct assignment + class task in same always_comb) + +class C0; + task automatic set1(ref logic q); + q = 1'b1; + endtask + task automatic set0(ref logic q); + q = 1'b0; + endtask +endclass + +module class0 #()( + input logic sel + ,output logic val +); + + logic l0; + C0 c; + + initial c = new; + + always_comb begin + l0 = 1'b0; + if (sel) begin + c.set1(l0); + end + end + + assign val = l0; + +endmodule + +//---------------------------------------------------------------------- +// class1: class task chain - nested method calls write through ref in same always_comb + +class C1; + task automatic inner(inout logic q); + q = 1'b1; + endtask + task automatic outer(inout logic q); + inner(q); + endtask +endclass + +module class1 #()( + input logic sel + ,output logic val +); + + logic l0; + C1 c; + + initial c = new; + + always_comb begin + l0 = 1'b0; + if (sel) begin + c.outer(l0); + end + end + + assign val = l0; + +endmodule + +//---------------------------------------------------------------------- +// class2: class handle passed through module port - class method writes through ref + +class C2; + task automatic set1(ref logic q); + q = 1'b1; + endtask +endclass + +module class2 #()( + input logic sel + ,output logic val + ,C2 c +); + + logic l0; + + always_comb begin + l0 = 1'b0; + if (sel) begin + c.set1(l0); + end + end + + assign val = l0; + +endmodule + +//---------------------------------------------------------------------- +// class3: static class task - call via class scope, writes through ref in same always_comb + +class C3; + static task automatic set1(ref logic q); + q = 1'b1; + endtask +endclass + +module class3 #()( + input logic sel + ,output logic val +); + + logic l0; + + always_comb begin + l0 = 1'b0; + if (sel) begin + C3::set1(l0); + end + end + + assign val = l0; + +endmodule + +//---------------------------------------------------------------------- +// class4: class composition - one class calls another task, ultimately writes through ref + +class C4Inner; + task automatic set1(ref logic q); + q = 1'b1; + endtask +endclass + +class C4Outer; + C4Inner inner; + function new(); + inner = new; + endfunction + task automatic set1(ref logic q); + inner.set1(q); + endtask +endclass + +module class4 #()( + input logic sel + ,output logic val +); + + logic l0; + C4Outer c; + + initial c = new; + + always_comb begin + l0 = 1'b0; + if (sel) begin + c.set1(l0); + end + end + + assign val = l0; + +endmodule + +//---------------------------------------------------------------------- +// classf0: class function returns value - always_comb writes var directly + via class function call + +class Cf0; + function automatic logic ret1(); + return 1'b1; + endfunction +endclass + +module classf0 #()( + input logic sel + ,output logic val +); + + logic l0; + Cf0 c; + + initial c = new; + + always_comb begin + l0 = 1'b0; + if (sel) begin + l0 = c.ret1(); + end + end + + assign val = l0; + +endmodule + +//---------------------------------------------------------------------- +// classf1: static class function returns value - always_comb uses class scope call + +class Cf1; + static function automatic logic ret1(); + return 1'b1; + endfunction +endclass + +module classf1 #()( + input logic sel + ,output logic val +); + + logic l0; + + always_comb begin + l0 = 1'b0; + if (sel) begin + l0 = Cf1::ret1(); + end + end + + assign val = l0; + +endmodule + +//---------------------------------------------------------------------- +// Shared TB + +module m_tb#()(); + + logic sel; + + logic val0, val1, val2, val3, val4, valf0, valf1; + + C2 c2; + initial c2 = new; + + class0 u0(.sel(sel), .val(val0)); + class1 u1(.sel(sel), .val(val1)); + class2 u2(.sel(sel), .val(val2), .c(c2)); + class3 u3(.sel(sel), .val(val3)); + class4 u4(.sel(sel), .val(val4)); + classf0 uf0(.sel(sel), .val(valf0)); + classf1 uf1(.sel(sel), .val(valf1)); + + task automatic check_all(input logic exp); + `checkd(val0, exp); + `checkd(val1, exp); + `checkd(val2, exp); + `checkd(val3, exp); + `checkd(val4, exp); + `checkd(valf0, exp); + `checkd(valf1, exp); + endtask + + initial begin + #1; + sel = 'b0; + #1; + check_all(1'b0); + + sel = 'b1; + #1; + check_all(1'b1); + + sel = 'b0; + #1; + check_all(1'b0); + end + + initial begin + #5; + $write("*-* All Finished *-*\n"); + $finish; + end + +endmodule diff --git a/test_regress/t/t_multidriven_funcret0.py b/test_regress/t/t_multidriven_funcret0.py new file mode 100755 index 000000000..c6e56559a --- /dev/null +++ b/test_regress/t/t_multidriven_funcret0.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# Copyright 2025 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() diff --git a/test_regress/t/t_multidriven_funcret0.v b/test_regress/t/t_multidriven_funcret0.v new file mode 100644 index 000000000..0e82e300b --- /dev/null +++ b/test_regress/t/t_multidriven_funcret0.v @@ -0,0 +1,43 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed into the Public Domain, for any use, +// without warranty. +// SPDX-License-Identifier: CC0-1.0 + +// MULTIDRIVEN false positive - package function return var +// +// Minimal reproducer for: package function with "return expr" used in always_comb expression. +// The function return variable must not be treated as a side-effect "writeSummary" target. + +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d (%s !== %s)\n", `__FILE__,`__LINE__, (gotv), (expv), `"gotv`", `"expv`"); `stop; end while(0); + +package p; + function automatic int num_bytes(input int size); + return 1 << size; + endfunction +endpackage + +module t; + typedef struct packed { + logic [31:0] addr; + logic [2:0] size; + } meta_t; + + meta_t rd_meta_q; + meta_t rd_meta; + + always_comb begin + rd_meta = rd_meta_q; + rd_meta.addr = rd_meta_q.addr + p::num_bytes(int'(rd_meta_q.size)); + end + + initial begin + rd_meta_q.addr = 32'h100; + rd_meta_q.size = 3'd2; // num_bytes = 4 + #1; + `checkd(rd_meta.addr, 32'h104); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_multidriven_iface.py b/test_regress/t/t_multidriven_iface.py new file mode 100755 index 000000000..c6e56559a --- /dev/null +++ b/test_regress/t/t_multidriven_iface.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# Copyright 2025 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() diff --git a/test_regress/t/t_multidriven_iface.v b/test_regress/t/t_multidriven_iface.v new file mode 100644 index 000000000..4a028ac9f --- /dev/null +++ b/test_regress/t/t_multidriven_iface.v @@ -0,0 +1,235 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed into the Public Domain, for any use, +// without warranty. +// SPDX-License-Identifier: CC0-1.0 + +// Consolidated interface-based multidriven tests +// (formerly t_multidriven_iface{0,1,2,3,4,5,6}.v) + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d (%s !== %s)\n", `__FILE__,`__LINE__, (gotv), (expv), `"gotv`", `"expv`"); `stop; end while(0); +// verilog_format: on + +//---------------------------------------------------------------------- +// iface0: direct assignment to interface signal + interface task assign in same process + +interface my_if0; + logic l0; + task set_l0_1(); l0 = 1'b1; endtask + task set_l0_0(); l0 = 1'b0; endtask +endinterface + +module iface0 #()( + input logic sel, + output logic val +); + my_if0 if0(); + always_comb begin + if0.l0 = 1'b0; + if (sel) begin + if0.set_l0_1(); + end + end + assign val = if0.l0; +endmodule + +//---------------------------------------------------------------------- +// iface1: interface task chain - nested calls write interface signal in same always_comb + +interface my_if1; + logic l0; + task set_l0_1_inner(); l0 = 1'b1; endtask + task set_l0_1_outer(); set_l0_1_inner(); endtask +endinterface + +module iface1 #()( + input logic sel, + output logic val +); + my_if1 if0(); + always_comb begin + if0.l0 = 1'b0; + if (sel) begin + if0.set_l0_1_outer(); + end + end + assign val = if0.l0; +endmodule + +//---------------------------------------------------------------------- +// iface2: interface passed through module port - direct assign + task call in same always_comb + +interface my_if2; + logic l0; + task set_l0_1(); l0 = 1'b1; endtask + task set_l0_0(); l0 = 1'b0; endtask +endinterface + +module iface2 #()( + input logic sel, + output logic val, + my_if2 ifp +); + always_comb begin + ifp.l0 = 1'b0; + if (sel) begin + ifp.set_l0_1(); + end + end + assign val = ifp.l0; +endmodule + +//---------------------------------------------------------------------- +// iface3: interface modport + task import - write interface signal in same always_comb + +interface my_if3; + logic l0; + task set_l0_1(); l0 = 1'b1; endtask + modport mp ( + output l0, + import set_l0_1 + ); +endinterface + +module iface3 #()( + input logic sel, + output logic val, + my_if3.mp ifp +); + always_comb begin + ifp.l0 = 1'b0; + if (sel) begin + ifp.set_l0_1(); + end + end + assign val = ifp.l0; +endmodule + +//---------------------------------------------------------------------- +// iface4: interface task writes through output formal - actual is interface member + +interface my_if4; + logic l0; + task automatic set_any(output logic q); + q = 1'b1; + endtask +endinterface + +module iface4 #()( + input logic sel, + output logic val +); + my_if4 if0(); + always_comb begin + if0.l0 = 1'b0; + if (sel) begin + if0.set_any(if0.l0); + end + end + assign val = if0.l0; +endmodule + +//---------------------------------------------------------------------- +// iface5: nested interface test - direct assignment + nested interface task call in same always_comb + +interface leaf_if5; + logic l0; + task set1(); l0 = 1'b1; endtask +endinterface + +interface top_if5; + leaf_if5 sub(); +endinterface + +module iface5 #()( + input logic sel, + output logic val +); + top_if5 if0(); + always_comb begin + if0.sub.l0 = 1'b0; + if (sel) begin + if0.sub.set1(); + end + end + assign val = if0.sub.l0; +endmodule + +//---------------------------------------------------------------------- +// iface6: nested interface aggregator - two nested interfaces, only one driven + +interface chan_if6; + logic b0; + task set1(); b0 = 1'b1; endtask +endinterface + +interface agg_if6; + chan_if6 tlb(); + chan_if6 ic(); +endinterface + +module iface6 #()( + input logic sel, + output logic val +); + agg_if6 a(); + always_comb begin + a.tlb.b0 = 1'b0; + if (sel) a.tlb.set1(); + end + assign val = a.tlb.b0; +endmodule + +//---------------------------------------------------------------------- +// Shared TB + +module m_tb#()(); + + logic sel; + logic val0, val1, val2, val3, val4, val5, val6; + + my_if2 if2(); + my_if3 if3(); + + iface0 u0(.sel(sel), .val(val0)); + iface1 u1(.sel(sel), .val(val1)); + iface2 u2(.sel(sel), .val(val2), .ifp(if2)); + iface3 u3(.sel(sel), .val(val3), .ifp(if3)); + iface4 u4(.sel(sel), .val(val4)); + iface5 u5(.sel(sel), .val(val5)); + iface6 u6(.sel(sel), .val(val6)); + + task automatic check_all(input logic exp); + `checkd(val0, exp); + `checkd(val1, exp); + `checkd(val2, exp); + `checkd(val3, exp); + `checkd(val4, exp); + `checkd(val5, exp); + `checkd(val6, exp); + endtask + + initial begin + #1; + sel = 'b0; + #1; + check_all(1'b0); + + sel = 'b1; + #1; + check_all(1'b1); + + sel = 'b0; + #1; + check_all(1'b0); + end + + initial begin + #5; + $write("*-* All Finished *-*\n"); + $finish; + end + +endmodule diff --git a/test_regress/t/t_multidriven_simple.py b/test_regress/t/t_multidriven_simple.py new file mode 100755 index 000000000..c6e56559a --- /dev/null +++ b/test_regress/t/t_multidriven_simple.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# Copyright 2025 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() diff --git a/test_regress/t/t_multidriven_simple.v b/test_regress/t/t_multidriven_simple.v new file mode 100644 index 000000000..e1a240578 --- /dev/null +++ b/test_regress/t/t_multidriven_simple.v @@ -0,0 +1,120 @@ + +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed into the Public Domain, for any use, +// without warranty. +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d (%s !== %s)\n", `__FILE__,`__LINE__, (gotv), (expv), `"gotv`", `"expv`"); `stop; end while(0); +// verilog_format: on + +// direct task call +module mod0 #()( + input logic sel, + output logic val +); + logic l0; + task do_stuff(); + l0 = 'b1; + endtask + always_comb begin + l0 = 'b0; + if(sel) begin + do_stuff(); + end + end + assign val = l0; +endmodule + +// nested task call chain +module mod1 #()( + input logic sel, + output logic val +); + logic l0; + task do_inner(); + l0 = 'b1; + endtask + task do_outer(); + do_inner(); + endtask + always_comb begin + l0 = 'b0; + if (sel) do_outer(); + end + assign val = l0; +endmodule + +// task writes through an output arguement +module mod2 #()( + input logic sel, + output logic val +); + logic l0; + task automatic do_stuff(output logic q); + q = 1'b1; + endtask + always_comb begin + l0 = 1'b0; + if (sel) do_stuff(l0); + end + assign val = l0; +endmodule + +// function call that writes +module mod3 #()( + input logic sel, + output logic val +); + logic l0; + function automatic void do_func(); + l0 = 1'b1; + endfunction + always_comb begin + l0 = 1'b0; + if (sel) do_func(); + end + assign val = l0; +endmodule + +// two tasks set0/set1 +module mod4 #()( + input logic sel, + output logic val +); + logic l0; + task automatic set1(); l0 = 1'b1; endtask + task automatic set0(); l0 = 1'b0; endtask + always_comb begin + set0(); + if (sel) begin + set1(); + end + end + assign val = l0; +endmodule + +module m_tb; + logic sel; + logic v0, v1, v2, v3, v4; + + mod0 u0(.sel(sel), .val(v0)); + mod1 u1(.sel(sel), .val(v1)); + mod2 u2(.sel(sel), .val(v2)); + mod3 u3(.sel(sel), .val(v3)); + mod4 u4(.sel(sel), .val(v4)); + + initial begin + #1; sel = 0; + `checkd(v0, 0); `checkd(v1, 0); `checkd(v2, 0); `checkd(v3, 0); `checkd(v4, 0); + #1; sel = 1; + `checkd(v0, 1); `checkd(v1, 1); `checkd(v2, 1); `checkd(v3, 1); `checkd(v4, 1); + #1; sel = 0; + `checkd(v0, 0); `checkd(v1, 0); `checkd(v2, 0); `checkd(v3, 0); `checkd(v4, 0); + #1; + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule