2026-04-22 21:18:59 +02:00
|
|
|
// -*- mode: C++; c-file-style: "cc-mode" -*-
|
|
|
|
|
//*************************************************************************
|
|
|
|
|
// DESCRIPTION: Verilator: FSM coverage detect pass
|
|
|
|
|
//
|
|
|
|
|
// Code available from: https://verilator.org
|
|
|
|
|
//
|
|
|
|
|
//*************************************************************************
|
|
|
|
|
//
|
|
|
|
|
// This program is free software; you can redistribute it and/or modify it
|
|
|
|
|
// under the terms of either the GNU Lesser General Public License Version 3
|
|
|
|
|
// or the Perl Artistic License Version 2.0.
|
|
|
|
|
// SPDX-FileCopyrightText: 2026 Wilson Snyder
|
|
|
|
|
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
|
|
|
|
|
//
|
|
|
|
|
//*************************************************************************
|
|
|
|
|
// FSM COVERAGE DETECT:
|
|
|
|
|
// Walk clocked always blocks while the original FSM structure is still
|
|
|
|
|
// present, build a per-FSM V3Graph representation of the extracted
|
|
|
|
|
// states/transitions, then immediately lower that completed graph state
|
|
|
|
|
// into the final coverage declarations, previous-state tracking, and
|
|
|
|
|
// active blocks needed to implement FSM state and arc coverage in the
|
|
|
|
|
// generated model.
|
|
|
|
|
//
|
|
|
|
|
//*************************************************************************
|
|
|
|
|
|
|
|
|
|
#include "V3PchAstNoMT.h"
|
|
|
|
|
|
|
|
|
|
#include "V3FsmDetect.h"
|
|
|
|
|
|
|
|
|
|
#include "V3Ast.h"
|
|
|
|
|
#include "V3Graph.h"
|
|
|
|
|
|
|
|
|
|
#include <cctype>
|
|
|
|
|
#include <map>
|
|
|
|
|
#include <memory>
|
|
|
|
|
#include <unordered_map>
|
2026-04-30 13:22:34 +02:00
|
|
|
#include <unordered_set>
|
2026-04-22 21:18:59 +02:00
|
|
|
|
|
|
|
|
VL_DEFINE_DEBUG_FUNCTIONS;
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
// Captures one sensitivity-list entry so the lowering phase can later rebuild
|
|
|
|
|
// an active block with the same triggering event control.
|
|
|
|
|
struct FsmSenDesc final {
|
|
|
|
|
// Encoded edge kind copied from AstSenItem::edgeType() so lowering can
|
|
|
|
|
// rebuild the same trigger semantics on the synthesized coverage block.
|
|
|
|
|
VEdgeType::en edgeType = static_cast<VEdgeType::en>(0);
|
|
|
|
|
// Triggering signal in the saved scoped AST.
|
|
|
|
|
AstVarScope* varScopep = nullptr;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Captures the simple reset predicate shape that survives to this pass after
|
|
|
|
|
// earlier normalization so reset arcs can be reconstructed during lowering.
|
|
|
|
|
struct FsmResetCondDesc final {
|
|
|
|
|
// Reset signal used by the FSM in the saved scoped AST.
|
|
|
|
|
AstVarScope* varScopep = nullptr;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-30 13:22:34 +02:00
|
|
|
class FsmResetArcDesc final {
|
|
|
|
|
int m_toValue = 0; // Encoded reset target state.
|
|
|
|
|
AstNode* m_nodep = nullptr; // Source node for warnings and emitted metadata.
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
FsmResetArcDesc() = default;
|
|
|
|
|
FsmResetArcDesc(int toValue, AstNode* nodep)
|
|
|
|
|
: m_toValue{toValue}
|
|
|
|
|
, m_nodep{nodep} {}
|
|
|
|
|
|
|
|
|
|
int toValue() const { return m_toValue; }
|
|
|
|
|
AstNode* nodep() const { return m_nodep; }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class FsmRegisterCandidate final {
|
|
|
|
|
AstScope* m_scopep = nullptr; // Owning scope for the paired FSM.
|
|
|
|
|
AstAlways* m_alwaysp = nullptr; // Register process that commits the state.
|
|
|
|
|
AstVarScope* m_stateVscp = nullptr; // Registered FSM state variable.
|
|
|
|
|
AstVarScope* m_nextVscp = nullptr; // Next-state variable or same state var for 1-block FSMs.
|
|
|
|
|
std::vector<FsmSenDesc> m_senses; // Event controls for recreated coverage blocks.
|
|
|
|
|
FsmResetCondDesc m_resetCond; // Saved reset predicate, if any.
|
|
|
|
|
std::vector<FsmResetArcDesc> m_resetArcs; // Reset target arcs recovered during detect.
|
|
|
|
|
bool m_hasResetCond = false; // Whether the FSM had a modeled reset predicate.
|
|
|
|
|
bool m_resetInclude = false; // Whether reset arcs count toward summary totals.
|
|
|
|
|
bool m_inclCond = false; // Whether conditional/default arcs are kept explicitly.
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
AstScope* scopep() const { return m_scopep; }
|
|
|
|
|
void scopep(AstScope* scopep) { m_scopep = scopep; }
|
|
|
|
|
AstAlways* alwaysp() const { return m_alwaysp; }
|
|
|
|
|
void alwaysp(AstAlways* alwaysp) { m_alwaysp = alwaysp; }
|
|
|
|
|
AstVarScope* stateVscp() const { return m_stateVscp; }
|
|
|
|
|
void stateVscp(AstVarScope* vscp) { m_stateVscp = vscp; }
|
|
|
|
|
AstVarScope* nextVscp() const { return m_nextVscp; }
|
|
|
|
|
void nextVscp(AstVarScope* vscp) { m_nextVscp = vscp; }
|
|
|
|
|
const std::vector<FsmSenDesc>& senses() const { return m_senses; }
|
|
|
|
|
std::vector<FsmSenDesc>& senses() { return m_senses; }
|
|
|
|
|
const FsmResetCondDesc& resetCond() const { return m_resetCond; }
|
|
|
|
|
FsmResetCondDesc& resetCond() { return m_resetCond; }
|
|
|
|
|
const std::vector<FsmResetArcDesc>& resetArcs() const { return m_resetArcs; }
|
|
|
|
|
std::vector<FsmResetArcDesc>& resetArcs() { return m_resetArcs; }
|
|
|
|
|
bool hasResetCond() const { return m_hasResetCond; }
|
|
|
|
|
void hasResetCond(bool flag) { m_hasResetCond = flag; }
|
|
|
|
|
bool resetInclude() const { return m_resetInclude; }
|
|
|
|
|
void resetInclude(bool flag) { m_resetInclude = flag; }
|
|
|
|
|
bool inclCond() const { return m_inclCond; }
|
|
|
|
|
void inclCond(bool flag) { m_inclCond = flag; }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class FsmComboAlways final {
|
|
|
|
|
AstScope* const m_scopep = nullptr; // Owning scope for the combinational process.
|
|
|
|
|
AstAlways* const m_alwaysp = nullptr; // Candidate transition process.
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
FsmComboAlways() = default;
|
|
|
|
|
FsmComboAlways(AstScope* scopep, AstAlways* alwaysp)
|
|
|
|
|
: m_scopep{scopep}
|
|
|
|
|
, m_alwaysp{alwaysp} {}
|
|
|
|
|
|
|
|
|
|
AstScope* scopep() const { return m_scopep; }
|
|
|
|
|
AstAlways* alwaysp() const { return m_alwaysp; }
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-22 21:18:59 +02:00
|
|
|
class FsmGraph;
|
|
|
|
|
|
|
|
|
|
class FsmVertex VL_NOT_FINAL : public V3GraphVertex {
|
|
|
|
|
VL_RTTI_IMPL(FsmVertex, V3GraphVertex)
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
enum class Kind : uint8_t { STATE, RESET_ANY, DEFAULT_ANY };
|
|
|
|
|
|
|
|
|
|
private:
|
2026-04-22 21:20:00 +02:00
|
|
|
Kind m_kind; // State vs synthetic ANY/default vertex role.
|
2026-04-22 21:18:59 +02:00
|
|
|
string m_label; // User-facing state or pseudo-state label.
|
|
|
|
|
int m_value = 0; // Encoded state value for real state vertices.
|
|
|
|
|
|
|
|
|
|
protected:
|
|
|
|
|
FsmVertex(V3Graph* graphp, Kind kind, string label, int value) VL_MT_DISABLED
|
2026-04-22 21:20:00 +02:00
|
|
|
: V3GraphVertex{graphp},
|
|
|
|
|
m_kind{kind},
|
|
|
|
|
m_label{label},
|
|
|
|
|
m_value{value} {}
|
2026-04-22 21:18:59 +02:00
|
|
|
~FsmVertex() override = default;
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
Kind kind() const { return m_kind; }
|
|
|
|
|
bool isState() const { return m_kind == Kind::STATE; }
|
|
|
|
|
bool isResetAny() const { return m_kind == Kind::RESET_ANY; }
|
|
|
|
|
bool isDefaultAny() const { return m_kind == Kind::DEFAULT_ANY; }
|
|
|
|
|
const string& label() const { return m_label; }
|
|
|
|
|
int value() const { return m_value; }
|
|
|
|
|
|
|
|
|
|
string name() const override VL_MT_SAFE { return m_label + "=" + cvtToStr(m_value); }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class FsmStateVertex final : public FsmVertex {
|
|
|
|
|
VL_RTTI_IMPL(FsmStateVertex, FsmVertex)
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
FsmStateVertex(V3Graph* graphp, string label, int value) VL_MT_DISABLED
|
|
|
|
|
: FsmVertex{graphp, Kind::STATE, label, value} {}
|
|
|
|
|
~FsmStateVertex() override = default;
|
|
|
|
|
|
|
|
|
|
string dotColor() const override { return "lightblue"; }
|
|
|
|
|
string dotShape() const override { return "ellipse"; }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class FsmPseudoVertex final : public FsmVertex {
|
|
|
|
|
VL_RTTI_IMPL(FsmPseudoVertex, FsmVertex)
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
FsmPseudoVertex(V3Graph* graphp, Kind kind, string label) VL_MT_DISABLED
|
|
|
|
|
: FsmVertex{graphp, kind, label, 0} {}
|
|
|
|
|
~FsmPseudoVertex() override = default;
|
|
|
|
|
|
|
|
|
|
string name() const override VL_MT_SAFE { return label(); }
|
|
|
|
|
string dotColor() const override { return isResetAny() ? "darkgreen" : "orange"; }
|
|
|
|
|
string dotShape() const override { return "diamond"; }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class FsmArcEdge final : public V3GraphEdge {
|
|
|
|
|
VL_RTTI_IMPL(FsmArcEdge, V3GraphEdge)
|
|
|
|
|
bool m_isReset = false; // Arc originates from the synthetic reset source.
|
2026-04-22 21:20:00 +02:00
|
|
|
bool m_isCond = false; // Arc came from a conditional next-state split.
|
2026-04-22 21:18:59 +02:00
|
|
|
bool m_isDefault = false; // Arc represents a case default source.
|
|
|
|
|
FileLine* m_flp = nullptr; // Source location for emitted coverage metadata.
|
|
|
|
|
|
|
|
|
|
public:
|
2026-04-22 21:20:00 +02:00
|
|
|
FsmArcEdge(V3Graph* graphp, FsmVertex* fromp, FsmStateVertex* top, bool isReset, bool isCond,
|
|
|
|
|
bool isDefault, FileLine* flp) VL_MT_DISABLED : V3GraphEdge{graphp, fromp, top, 1},
|
|
|
|
|
m_isReset{isReset},
|
|
|
|
|
m_isCond{isCond},
|
|
|
|
|
m_isDefault{isDefault},
|
|
|
|
|
m_flp{flp} {}
|
2026-04-22 21:18:59 +02:00
|
|
|
~FsmArcEdge() override = default;
|
|
|
|
|
|
|
|
|
|
bool isReset() const { return m_isReset; }
|
|
|
|
|
bool isCond() const { return m_isCond; }
|
|
|
|
|
bool isDefault() const { return m_isDefault; }
|
|
|
|
|
FileLine* fileline() const { return m_flp; }
|
|
|
|
|
|
|
|
|
|
string dotLabel() const override {
|
|
|
|
|
if (m_isReset) return "reset";
|
|
|
|
|
if (m_isDefault) return "default";
|
|
|
|
|
if (m_isCond) return "cond";
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
string dotColor() const override {
|
|
|
|
|
if (m_isReset) return "darkgreen";
|
|
|
|
|
if (m_isDefault) return "orange";
|
|
|
|
|
if (m_isCond) return "blue";
|
|
|
|
|
return "black";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// One graph per detected FSM. Graph-level metadata captures the non-graph
|
|
|
|
|
// context needed to lower states/arcs back into the AST after detection.
|
|
|
|
|
class FsmGraph final : public V3Graph {
|
|
|
|
|
AstScope* m_scopep = nullptr; // Owning scoped block for the detected FSM.
|
2026-04-30 13:22:34 +02:00
|
|
|
AstAlways* m_stateAlwaysp = nullptr; // Register always block being instrumented.
|
2026-04-22 21:18:59 +02:00
|
|
|
string m_stateVarName; // Pretty state variable name for user-visible output.
|
|
|
|
|
string m_stateVarInternalName; // Internal state symbol name for dump tags.
|
|
|
|
|
AstVarScope* m_stateVarScopep = nullptr; // Scoped state variable being tracked.
|
|
|
|
|
std::vector<FsmSenDesc> m_senses; // Saved event controls for recreated active blocks.
|
|
|
|
|
FsmResetCondDesc m_resetCond; // Saved reset predicate shape, if one exists.
|
2026-04-22 21:20:00 +02:00
|
|
|
bool m_hasResetCond = false; // Whether the detected FSM had a reset branch.
|
2026-04-22 21:18:59 +02:00
|
|
|
bool m_resetInclude = false; // Whether reset arcs count toward coverage totals.
|
2026-04-22 21:20:00 +02:00
|
|
|
bool m_inclCond = false; // Whether conditional arcs should be kept explicitly.
|
|
|
|
|
FileLine* m_flp = nullptr; // Representative source location for declarations/arcs.
|
2026-04-22 21:18:59 +02:00
|
|
|
std::unordered_map<int, FsmStateVertex*> m_stateVertices; // Value to state-vertex map.
|
|
|
|
|
FsmPseudoVertex* m_resetVertexp = nullptr; // Synthetic ANY source for reset arcs.
|
|
|
|
|
FsmPseudoVertex* m_defaultVertexp = nullptr; // Synthetic default source for case defaults.
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
FsmGraph() VL_MT_DISABLED
|
2026-04-22 21:20:00 +02:00
|
|
|
: m_resetVertexp{new FsmPseudoVertex{this, FsmVertex::Kind::RESET_ANY, "ANY"}},
|
|
|
|
|
m_defaultVertexp{new FsmPseudoVertex{this, FsmVertex::Kind::DEFAULT_ANY, "default"}} {}
|
2026-04-22 21:18:59 +02:00
|
|
|
|
|
|
|
|
AstScope* scopep() const { return m_scopep; }
|
|
|
|
|
void scopep(AstScope* scopep) { m_scopep = scopep; }
|
2026-04-30 13:22:34 +02:00
|
|
|
AstAlways* stateAlwaysp() const { return m_stateAlwaysp; }
|
|
|
|
|
void stateAlwaysp(AstAlways* alwaysp) { m_stateAlwaysp = alwaysp; }
|
2026-04-22 21:18:59 +02:00
|
|
|
const string& stateVarName() const { return m_stateVarName; }
|
|
|
|
|
void stateVarName(const string& name) { m_stateVarName = name; }
|
|
|
|
|
const string& stateVarInternalName() const { return m_stateVarInternalName; }
|
|
|
|
|
void stateVarInternalName(const string& name) { m_stateVarInternalName = name; }
|
|
|
|
|
AstVarScope* stateVarScopep() const { return m_stateVarScopep; }
|
|
|
|
|
void stateVarScopep(AstVarScope* vscp) { m_stateVarScopep = vscp; }
|
|
|
|
|
const std::vector<FsmSenDesc>& senses() const { return m_senses; }
|
|
|
|
|
std::vector<FsmSenDesc>& senses() { return m_senses; }
|
|
|
|
|
const FsmResetCondDesc& resetCond() const { return m_resetCond; }
|
|
|
|
|
FsmResetCondDesc& resetCond() { return m_resetCond; }
|
|
|
|
|
bool hasResetCond() const { return m_hasResetCond; }
|
|
|
|
|
void hasResetCond(bool flag) { m_hasResetCond = flag; }
|
|
|
|
|
bool resetInclude() const { return m_resetInclude; }
|
|
|
|
|
void resetInclude(bool flag) { m_resetInclude = flag; }
|
|
|
|
|
bool inclCond() const { return m_inclCond; }
|
|
|
|
|
void inclCond(bool flag) { m_inclCond = flag; }
|
|
|
|
|
FileLine* fileline() const { return m_flp; }
|
|
|
|
|
void fileline(FileLine* flp) { m_flp = flp; }
|
|
|
|
|
|
|
|
|
|
FsmStateVertex* addStateVertex(string label, int value) VL_MT_DISABLED {
|
|
|
|
|
FsmStateVertex* const vertexp = new FsmStateVertex{this, label, value};
|
|
|
|
|
m_stateVertices.emplace(value, vertexp);
|
|
|
|
|
return vertexp;
|
|
|
|
|
}
|
|
|
|
|
FsmPseudoVertex* resetAnyVertex() VL_MT_DISABLED { return m_resetVertexp; }
|
|
|
|
|
FsmPseudoVertex* defaultAnyVertex() VL_MT_DISABLED { return m_defaultVertexp; }
|
|
|
|
|
FsmArcEdge* addArc(int fromValue, int toValue, bool isReset, bool isCond, bool isDefault,
|
|
|
|
|
FileLine* flp) VL_MT_DISABLED {
|
|
|
|
|
FsmStateVertex* const top = m_stateVertices.at(toValue);
|
|
|
|
|
FsmVertex* fromp = nullptr;
|
|
|
|
|
if (isReset) {
|
|
|
|
|
fromp = resetAnyVertex();
|
|
|
|
|
} else if (isDefault) {
|
|
|
|
|
fromp = defaultAnyVertex();
|
|
|
|
|
} else {
|
|
|
|
|
fromp = m_stateVertices.at(fromValue);
|
|
|
|
|
}
|
|
|
|
|
return new FsmArcEdge{this, fromp, top, isReset, isCond, isDefault, flp};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string name() const VL_MT_SAFE {
|
|
|
|
|
return "FSM "
|
|
|
|
|
+ (m_stateVarName.empty() ? (m_stateVarScopep ? m_stateVarScopep->name() : "")
|
|
|
|
|
: m_stateVarName);
|
|
|
|
|
}
|
|
|
|
|
string dumpTag(size_t index) const {
|
|
|
|
|
string tag = stateVarInternalName();
|
|
|
|
|
for (char& ch : tag) {
|
|
|
|
|
if (!std::isalnum(static_cast<unsigned char>(ch))) ch = '_';
|
|
|
|
|
}
|
|
|
|
|
return "fsm_" + cvtToStr(index) + "_" + tag;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct DetectedFsm final {
|
|
|
|
|
std::unique_ptr<FsmGraph> graphp; // Extracted graph for one detected FSM candidate.
|
|
|
|
|
};
|
2026-04-30 13:22:34 +02:00
|
|
|
using DetectedFsmMap = std::map<const AstVarScope*, DetectedFsm>;
|
|
|
|
|
|
|
|
|
|
struct FsmCaseCandidate final {
|
|
|
|
|
AstNode* warnNodep = nullptr; // Transition node that made the candidate supported.
|
|
|
|
|
AstVarScope* stateVscp = nullptr; // FSM state variable associated with that candidate.
|
|
|
|
|
};
|
2026-04-22 21:18:59 +02:00
|
|
|
|
2026-05-07 12:53:19 +02:00
|
|
|
struct StateConstLabel final {
|
|
|
|
|
string text;
|
|
|
|
|
bool fromParam = false;
|
|
|
|
|
size_t stateIndex = 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct FsmStateSpace final {
|
|
|
|
|
std::vector<std::pair<string, int>> states; // User label and encoded value.
|
|
|
|
|
std::unordered_map<int, StateConstLabel> labels; // Encoded value to user label.
|
|
|
|
|
string stateVarName; // Pretty tracked FSM state variable name.
|
|
|
|
|
bool enumBacked = false; // Whether states came from an enum declaration.
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-22 21:18:59 +02:00
|
|
|
// Local shared state between the two adjacent FSM coverage phases. Detection
|
|
|
|
|
// fills this with recovered FSM graphs; lowering consumes the completed graphs
|
|
|
|
|
// immediately afterward without needing any AST serialization bridge.
|
|
|
|
|
class FsmState final {
|
2026-04-30 13:22:34 +02:00
|
|
|
// All detected FSMs keyed by state varscope identity. This is the only bridge
|
2026-04-22 21:18:59 +02:00
|
|
|
// between the adjacent detect and lower phases, so the second phase never
|
|
|
|
|
// needs to rediscover or serialize the extracted machine.
|
|
|
|
|
DetectedFsmMap m_fsms;
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
DetectedFsmMap& fsms() { return m_fsms; }
|
|
|
|
|
const DetectedFsmMap& fsms() const { return m_fsms; }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Detection runs while the original clocked/case structure is still intact and
|
|
|
|
|
// populates graph-backed FSM models without mutating the tree mid-traversal.
|
|
|
|
|
// This pass is intentionally conservative: for this PR we only lock down the
|
|
|
|
|
// small set of transition/selector forms that are already stable in the
|
|
|
|
|
// normalized AST we see here. The remaining reject branches are therefore
|
|
|
|
|
// mostly future-feature boundaries, not accidental dead code.
|
|
|
|
|
class FsmDetectVisitor final : public VNVisitor {
|
|
|
|
|
// STATE - for current visit position (use VL_RESTORER)
|
|
|
|
|
FsmState& m_state;
|
|
|
|
|
AstScope* m_scopep = nullptr;
|
2026-04-30 13:22:34 +02:00
|
|
|
std::unordered_map<const AstVarScope*, FsmRegisterCandidate> m_registerCandidates;
|
|
|
|
|
std::vector<FsmComboAlways> m_comboAlwayss;
|
|
|
|
|
std::vector<FsmComboAlways> m_nonComboAlwayss;
|
|
|
|
|
std::unordered_map<const AstVarScope*, FsmCaseCandidate> m_comboPaired;
|
2026-04-22 21:18:59 +02:00
|
|
|
|
|
|
|
|
// METHODS
|
|
|
|
|
// Enum-backed FSMs may be wrapped in refs/typedefs; normalize to the
|
|
|
|
|
// underlying enum type before deciding whether a case is a candidate.
|
|
|
|
|
static AstNodeDType* unwrapEnumCandidate(AstNodeDType* dtypep) {
|
|
|
|
|
return dtypep->skipRefToEnump();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 13:22:34 +02:00
|
|
|
static string candidateConflictContext(AstNode* laterNodep,
|
|
|
|
|
const FsmCaseCandidate& firstCand) {
|
|
|
|
|
return '\n' + laterNodep->warnContextPrimary() + firstCand.warnNodep->warnOther()
|
|
|
|
|
+ "... Location of first supported candidate for "
|
|
|
|
|
+ firstCand.stateVscp->prettyNameQ() + '\n'
|
|
|
|
|
+ firstCand.warnNodep->warnContextSecondary();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class RegisterAlwaysAnalyzer final {
|
|
|
|
|
AstScope* const m_scopep;
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
explicit RegisterAlwaysAnalyzer(AstScope* scopep)
|
|
|
|
|
: m_scopep{scopep} {}
|
|
|
|
|
|
2026-04-30 13:23:36 +02:00
|
|
|
std::vector<std::pair<AstCase*, AstNodeExpr*>>
|
|
|
|
|
oneBlockCandidates(AstAlways* alwaysp) const {
|
2026-04-30 13:22:34 +02:00
|
|
|
std::vector<std::pair<AstCase*, AstNodeExpr*>> candidates;
|
|
|
|
|
AstNode* const stmtsp = alwaysp->stmtsp();
|
|
|
|
|
AstIf* const firstIfp = VN_CAST(stmtsp, If);
|
|
|
|
|
if (firstIfp) {
|
|
|
|
|
if (AstCase* const casep = VN_CAST(firstIfp->elsesp(), Case)) {
|
2026-04-30 13:23:36 +02:00
|
|
|
candidates.emplace_back(casep,
|
|
|
|
|
FsmDetectVisitor::isSimpleResetCond(firstIfp->condp())
|
|
|
|
|
? firstIfp->condp()
|
|
|
|
|
: nullptr);
|
2026-04-30 13:22:34 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (AstNode* nodep = stmtsp; nodep; nodep = nodep->nextp()) {
|
|
|
|
|
if (AstCase* const casep = VN_CAST(nodep, Case))
|
|
|
|
|
candidates.emplace_back(casep, nullptr);
|
|
|
|
|
}
|
|
|
|
|
return candidates;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool matchRegisterCandidate(AstAlways* alwaysp, FsmRegisterCandidate& cand) const {
|
|
|
|
|
return FsmDetectVisitor::matchRegisterAlways(alwaysp, m_scopep, cand);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 13:23:36 +02:00
|
|
|
void buildOneBlockCandidate(AstAlways* alwaysp, AstVarScope* vscp, AstNodeExpr* resetCondp,
|
|
|
|
|
FsmRegisterCandidate& reg) const {
|
2026-04-30 13:22:34 +02:00
|
|
|
reg.scopep(m_scopep);
|
|
|
|
|
reg.alwaysp(alwaysp);
|
|
|
|
|
reg.stateVscp(vscp);
|
|
|
|
|
reg.nextVscp(vscp);
|
|
|
|
|
reg.senses() = FsmDetectVisitor::describeSenTree(alwaysp->sentreep());
|
|
|
|
|
reg.resetCond() = FsmDetectVisitor::describeResetCond(resetCondp);
|
|
|
|
|
reg.hasResetCond(reg.resetCond().varScopep != nullptr);
|
|
|
|
|
reg.resetInclude(vscp->varp()->attrFsmResetArc());
|
|
|
|
|
reg.inclCond(vscp->varp()->attrFsmArcInclCond());
|
|
|
|
|
AstIf* const firstIfp = VN_CAST(alwaysp->stmtsp(), If);
|
|
|
|
|
if (firstIfp && reg.hasResetCond()) {
|
|
|
|
|
AstVarScope* resetStateVscp = nullptr;
|
|
|
|
|
const ResetAssignStatus resetStatus = FsmDetectVisitor::collectConstStateAssigns(
|
|
|
|
|
firstIfp->thensp(), resetStateVscp, reg.resetArcs());
|
|
|
|
|
if (resetStatus == ResetAssignStatus::NONE || resetStateVscp != vscp) {
|
|
|
|
|
reg.resetArcs().clear();
|
|
|
|
|
int resetValue = 0;
|
|
|
|
|
AstNode* const thenNodep
|
|
|
|
|
= FsmDetectVisitor::singleMeaningfulBranch(firstIfp->thensp());
|
|
|
|
|
UASSERT_OBJ(thenNodep, firstIfp,
|
|
|
|
|
"one-block reset fallback requires a non-empty reset branch");
|
|
|
|
|
if (FsmDetectVisitor::directConstStateAssignNode(thenNodep, resetStateVscp,
|
|
|
|
|
resetValue)
|
|
|
|
|
&& resetStateVscp == vscp) {
|
|
|
|
|
reg.resetArcs().emplace_back(resetValue, firstIfp->thensp());
|
|
|
|
|
}
|
|
|
|
|
} else if (resetStatus == ResetAssignStatus::MULTI_SAME_STATE) {
|
|
|
|
|
reg.resetArcs().clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class ComboAlwaysAnalyzer final {
|
|
|
|
|
public:
|
|
|
|
|
struct ComboMatch final {
|
|
|
|
|
const FsmRegisterCandidate* matchedp = nullptr;
|
|
|
|
|
AstNode* warnNodep = nullptr;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
const std::unordered_map<const AstVarScope*, FsmRegisterCandidate>& m_registerCandidates;
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
explicit ComboAlwaysAnalyzer(
|
|
|
|
|
const std::unordered_map<const AstVarScope*, FsmRegisterCandidate>& registerCandidates)
|
|
|
|
|
: m_registerCandidates{registerCandidates} {}
|
|
|
|
|
|
|
|
|
|
ComboMatch matchCase(AstNode* stmtsp, AstCase* casep) const {
|
|
|
|
|
ComboMatch match;
|
|
|
|
|
AstVarRef* const selp = VN_CAST(casep->exprp(), VarRef);
|
|
|
|
|
if (!selp) return match;
|
|
|
|
|
for (const auto& it : m_registerCandidates) {
|
|
|
|
|
const FsmRegisterCandidate& reg = it.second;
|
|
|
|
|
if (selp->varScopep() == reg.nextVscp()) {
|
|
|
|
|
if (!FsmDetectVisitor::hasCanonicalNextStateDefaultBeforeCase(
|
|
|
|
|
stmtsp, casep, reg.stateVscp(), reg.nextVscp())) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
} else if (selp->varScopep() != reg.stateVscp()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-04-30 13:23:36 +02:00
|
|
|
AstNode* const warnNodep = FsmDetectVisitor::caseSupportedTransitionNode(
|
|
|
|
|
casep, reg.nextVscp(), reg.inclCond());
|
2026-04-30 13:22:34 +02:00
|
|
|
if (!warnNodep) continue;
|
|
|
|
|
match.matchedp = ®
|
|
|
|
|
match.warnNodep = warnNodep;
|
|
|
|
|
}
|
|
|
|
|
return match;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool shouldWarnUnsupported(AstNode* stmtsp, AstCase* casep) const {
|
|
|
|
|
const AstVarRef* const selp = VN_CAST(casep->exprp(), VarRef);
|
|
|
|
|
if (!selp) return false;
|
|
|
|
|
|
|
|
|
|
const auto isRecognizedFsm = [&](const auto& entry) -> bool {
|
|
|
|
|
const FsmRegisterCandidate& reg = entry.second;
|
|
|
|
|
const bool matchesNext = selp->varScopep() == reg.nextVscp();
|
|
|
|
|
const bool matchesState = selp->varScopep() == reg.stateVscp();
|
|
|
|
|
|
|
|
|
|
if (!matchesNext && !matchesState) return false;
|
|
|
|
|
if (matchesNext
|
|
|
|
|
&& !FsmDetectVisitor::hasCanonicalNextStateDefaultBeforeCase(
|
|
|
|
|
stmtsp, casep, reg.stateVscp(), reg.nextVscp())) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return FsmDetectVisitor::caseSupportedTransitionNode(casep, reg.nextVscp(),
|
|
|
|
|
reg.inclCond());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return std::any_of(m_registerCandidates.begin(), m_registerCandidates.end(),
|
|
|
|
|
isRecognizedFsm);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-22 21:18:59 +02:00
|
|
|
// Reset arcs are only modeled for the simple signal form that survives to
|
|
|
|
|
// this pass after earlier normalization.
|
2026-04-22 21:20:00 +02:00
|
|
|
static bool isSimpleResetCond(AstNodeExpr* condp) { return VN_IS(condp, VarRef); }
|
2026-04-22 21:18:59 +02:00
|
|
|
|
|
|
|
|
// Normalize the reset condition into a compact description so the lowering
|
|
|
|
|
// phase can regenerate the same predicate after detection. By the time
|
|
|
|
|
// this pass runs, active-low source forms such as "!rst_n" have already
|
|
|
|
|
// been canonicalized to a positive-condition if/else shape, so only a
|
|
|
|
|
// plain VarRef survives here.
|
|
|
|
|
static FsmResetCondDesc describeResetCond(AstNodeExpr* condp) {
|
|
|
|
|
FsmResetCondDesc desc;
|
|
|
|
|
if (AstVarRef* const vrefp = VN_CAST(condp, VarRef)) {
|
|
|
|
|
desc.varScopep = vrefp->varScopep();
|
|
|
|
|
}
|
|
|
|
|
return desc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Snapshot the original event control so the lowering phase can rebuild an
|
|
|
|
|
// active block with the same edge semantics.
|
|
|
|
|
static std::vector<FsmSenDesc> describeSenTree(AstSenTree* sentreep) {
|
|
|
|
|
std::vector<FsmSenDesc> senses;
|
|
|
|
|
for (AstSenItem* itemp = sentreep->sensesp(); itemp;
|
|
|
|
|
itemp = VN_AS(itemp->nextp(), SenItem)) {
|
|
|
|
|
AstNodeVarRef* const vrefp = itemp->varrefp();
|
|
|
|
|
if (!vrefp) continue;
|
|
|
|
|
FsmSenDesc desc;
|
|
|
|
|
desc.edgeType = itemp->edgeType().m_e;
|
|
|
|
|
desc.varScopep = vrefp->varScopep();
|
|
|
|
|
senses.push_back(desc);
|
|
|
|
|
}
|
|
|
|
|
return senses;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ignore existing coverage increments so FSM detection sees the user logic
|
|
|
|
|
// rather than other instrumentation already attached to the block.
|
|
|
|
|
static bool isIgnorableStmt(AstNode* nodep) { return VN_IS(nodep, CoverInc); }
|
|
|
|
|
|
2026-04-30 13:22:34 +02:00
|
|
|
static AstNode* skipLeadingIgnorableStmt(AstNode* nodep) {
|
|
|
|
|
while (nodep && isIgnorableStmt(nodep)) nodep = nodep->nextp();
|
|
|
|
|
return nodep;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Conservative extractor for statement lists: only treat a list as simple
|
|
|
|
|
// when exactly one non-coverage statement remains after unwrapping.
|
|
|
|
|
// Richer multi-statement or control-flow forms are intentionally left for
|
|
|
|
|
// follow-on FSM-detection work instead of being partially inferred here.
|
2026-04-22 21:18:59 +02:00
|
|
|
static AstNode* singleMeaningfulStmt(AstNode* stmtp) {
|
|
|
|
|
AstNode* resultp = nullptr;
|
|
|
|
|
for (AstNode* nodep = stmtp; nodep; nodep = nodep->nextp()) {
|
|
|
|
|
if (isIgnorableStmt(nodep)) continue;
|
|
|
|
|
if (resultp) return nullptr;
|
|
|
|
|
resultp = nodep;
|
|
|
|
|
}
|
|
|
|
|
return resultp;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 13:22:34 +02:00
|
|
|
// If/else branches are a single subtree, not a statement list, so do not
|
|
|
|
|
// walk nextp() here or we may accidentally consume the sibling else-arm.
|
|
|
|
|
static AstNode* singleMeaningfulBranch(AstNode* branchp) {
|
|
|
|
|
if (!branchp) return nullptr;
|
|
|
|
|
return branchp;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// By fsm-detect time, non-clocked always @* blocks are already admitted through
|
|
|
|
|
// a missing sentree. This helper therefore only needs to recognize
|
|
|
|
|
// explicit changed-sensitivity lists such as always @(a or b); clocked and
|
|
|
|
|
// event-driven forms remain out of scope.
|
|
|
|
|
static bool isPlainComboSentree(const AstSenTree* sentreep) {
|
|
|
|
|
UASSERT(sentreep, "plain combo sensitivity check requires a sensitivity tree");
|
|
|
|
|
for (const AstSenItem* senp = sentreep->sensesp(); senp;
|
|
|
|
|
senp = VN_AS(senp->nextp(), SenItem)) {
|
|
|
|
|
if (senp->edgeType() == VEdgeType::ET_CHANGED) continue;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void warnUnsupportedComboAlways(const FsmComboAlways& combo) {
|
|
|
|
|
const ComboAlwaysAnalyzer analyzer{m_registerCandidates};
|
|
|
|
|
AstNode* const stmtsp = skipLeadingIgnorableStmt(combo.alwaysp()->stmtsp());
|
|
|
|
|
bool warned = false;
|
|
|
|
|
for (AstNode* nodep = stmtsp; nodep; nodep = nodep->nextp()) {
|
|
|
|
|
AstCase* const casep = VN_CAST(nodep, Case);
|
|
|
|
|
if (!casep) continue;
|
|
|
|
|
if (analyzer.shouldWarnUnsupported(stmtsp, casep)) {
|
|
|
|
|
casep->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on non-clocked always "
|
|
|
|
|
"blocks requires a combinational sensitivity list or "
|
|
|
|
|
"always_comb");
|
|
|
|
|
warned = true;
|
|
|
|
|
}
|
|
|
|
|
if (warned) break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Case-item bodies are single subtrees like if/else arms, not statement
|
|
|
|
|
// lists, so unwrap only local begin/end wrappers here rather than walking
|
|
|
|
|
// sibling case items via nextp().
|
2026-04-22 21:18:59 +02:00
|
|
|
static AstNodeAssign* directStateAssign(AstNode* stmtp, AstVarScope* stateVscp) {
|
2026-04-30 13:22:34 +02:00
|
|
|
AstNode* const nodep = singleMeaningfulBranch(stmtp);
|
2026-04-22 21:18:59 +02:00
|
|
|
if (!nodep) return nullptr;
|
|
|
|
|
AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign);
|
|
|
|
|
if (!assp) return nullptr;
|
|
|
|
|
AstVarRef* const vrefp = VN_CAST(assp->lhsp(), VarRef);
|
|
|
|
|
if (!vrefp || vrefp->varScopep() != stateVscp) return nullptr;
|
|
|
|
|
return assp;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 13:22:34 +02:00
|
|
|
static AstNodeAssign* nodeStateVarAssign(AstNode* nodep, AstVarScope*& stateVscp,
|
|
|
|
|
AstVarScope*& fromVscp) {
|
|
|
|
|
AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign);
|
|
|
|
|
if (!assp) return nullptr;
|
|
|
|
|
AstVarRef* const lhsp = VN_AS(assp->lhsp(), VarRef);
|
|
|
|
|
UASSERT_OBJ(lhsp, assp, "register commit lhs should be normalized to a VarRef");
|
|
|
|
|
AstVarRef* const rhsp = VN_CAST(assp->rhsp(), VarRef);
|
|
|
|
|
if (!rhsp) return nullptr;
|
|
|
|
|
stateVscp = lhsp->varScopep();
|
|
|
|
|
fromVscp = rhsp->varScopep();
|
|
|
|
|
return assp;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static AstNodeAssign* directCondStateVarAssign(AstNode* nodep, AstVarScope*& stateVscp,
|
|
|
|
|
AstVarScope*& fromVscp, AstNodeExpr*& condp,
|
|
|
|
|
int& resetValue) {
|
|
|
|
|
AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign);
|
|
|
|
|
if (!assp) return nullptr;
|
|
|
|
|
AstVarRef* const lhsp = VN_AS(assp->lhsp(), VarRef);
|
|
|
|
|
UASSERT_OBJ(lhsp, assp,
|
|
|
|
|
"conditional register commit lhs should be normalized to a VarRef");
|
|
|
|
|
AstCond* const rhsp = VN_CAST(assp->rhsp(), Cond);
|
|
|
|
|
if (!rhsp) return nullptr;
|
|
|
|
|
AstVarRef* const elsep = VN_CAST(rhsp->elsep(), VarRef);
|
2026-05-07 12:53:19 +02:00
|
|
|
if (!elsep || constValueStatus(rhsp->thenp(), resetValue) != ConstValueStatus::OK) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
2026-04-30 13:22:34 +02:00
|
|
|
stateVscp = lhsp->varScopep();
|
|
|
|
|
fromVscp = elsep->varScopep();
|
|
|
|
|
condp = rhsp->condp();
|
|
|
|
|
return assp;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static AstNodeAssign* directConstStateAssignNode(AstNode* nodep, AstVarScope*& stateVscp,
|
|
|
|
|
int& value) {
|
|
|
|
|
AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign);
|
|
|
|
|
if (!assp) return nullptr;
|
|
|
|
|
AstVarRef* const lhsp = VN_AS(assp->lhsp(), VarRef);
|
|
|
|
|
UASSERT_OBJ(lhsp, assp,
|
|
|
|
|
"direct constant state assignment lhs should be normalized to a VarRef");
|
2026-05-07 12:53:19 +02:00
|
|
|
if (constValueStatus(assp->rhsp(), value) != ConstValueStatus::OK) return nullptr;
|
2026-04-30 13:22:34 +02:00
|
|
|
stateVscp = lhsp->varScopep();
|
|
|
|
|
return assp;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum class ResetAssignStatus : uint8_t {
|
|
|
|
|
NONE, // Reset branch was not the supported direct-constant shape.
|
|
|
|
|
SINGLE, // Exactly one supported reset assignment was collected.
|
|
|
|
|
MULTI_SAME_STATE // Multiple assignments to the same FSM state var; warn and ignore.
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Reset arcs are only extracted from the single direct-constant form. If
|
|
|
|
|
// user RTL assigns the same state register multiple times in the reset
|
|
|
|
|
// branch, warn and skip reset-arc modeling rather than inventing multiple
|
|
|
|
|
// reset transitions for an odd but legal coding style.
|
|
|
|
|
static ResetAssignStatus collectConstStateAssigns(AstNode* stmtp, AstVarScope*& stateVscp,
|
|
|
|
|
std::vector<FsmResetArcDesc>& resetArcs) {
|
|
|
|
|
AstNode* nodep = skipLeadingIgnorableStmt(stmtp);
|
|
|
|
|
UASSERT_OBJ(nodep, stmtp, "Empty reset branch unexpectedly survived to FSM detection");
|
|
|
|
|
for (;; nodep = nodep->nextp()) {
|
|
|
|
|
AstVarScope* assignStateVscp = nullptr;
|
|
|
|
|
int value = 0;
|
|
|
|
|
AstNodeAssign* const assp = directConstStateAssignNode(nodep, assignStateVscp, value);
|
|
|
|
|
if (!assp) return ResetAssignStatus::NONE;
|
|
|
|
|
if (!stateVscp) stateVscp = assignStateVscp;
|
|
|
|
|
if (assignStateVscp != stateVscp) return ResetAssignStatus::NONE;
|
|
|
|
|
if (!resetArcs.empty()) {
|
|
|
|
|
assp->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on reset branches with "
|
|
|
|
|
"multiple assignments to the state variable");
|
|
|
|
|
resetArcs.clear();
|
|
|
|
|
return ResetAssignStatus::MULTI_SAME_STATE;
|
|
|
|
|
}
|
|
|
|
|
resetArcs.emplace_back(value, assp);
|
|
|
|
|
if (!nodep->nextp()) return ResetAssignStatus::SINGLE;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool hasCanonicalNextStateDefaultBeforeCase(AstNode* stmtsp, AstCase* casep,
|
|
|
|
|
AstVarScope* stateVscp,
|
|
|
|
|
AstVarScope* nextVscp) {
|
|
|
|
|
AstNode* const bodyp = skipLeadingIgnorableStmt(stmtsp);
|
|
|
|
|
bool sawCanonicalDefault = false;
|
|
|
|
|
for (AstNode* nodep = bodyp;; nodep = nodep->nextp()) {
|
|
|
|
|
UASSERT_OBJ(nodep, casep,
|
|
|
|
|
"case(state_d) candidate not found in scanned statement list");
|
|
|
|
|
if (nodep == casep) return sawCanonicalDefault;
|
|
|
|
|
if (AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign)) {
|
|
|
|
|
AstVarRef* const lhsp = VN_CAST(assp->lhsp(), VarRef);
|
|
|
|
|
AstVarRef* const rhsp = VN_CAST(assp->rhsp(), VarRef);
|
|
|
|
|
if (!lhsp || lhsp->varScopep() != nextVscp) continue;
|
|
|
|
|
if (sawCanonicalDefault) {
|
|
|
|
|
const string nextName = nextVscp->varp()->prettyNameQ();
|
|
|
|
|
const string stateName = stateVscp->varp()->prettyNameQ();
|
|
|
|
|
assp->v3warn(COVERIGN,
|
|
|
|
|
"Ignoring unsupported: FSM coverage on case(" + nextName
|
|
|
|
|
+ ") when the canonical " + nextName + " = " + stateName
|
|
|
|
|
+ " default is overwritten before the case statement");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (!rhsp || rhsp->varScopep() != stateVscp) return false;
|
|
|
|
|
sawCanonicalDefault = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool ifStateConstAssign(AstNode* stmtp, AstVarScope* stateVscp, int& thenValue,
|
|
|
|
|
int& elseValue) {
|
|
|
|
|
AstIf* const ifp = VN_CAST(singleMeaningfulBranch(stmtp), If);
|
|
|
|
|
if (!ifp || !ifp->elsesp()) return false;
|
|
|
|
|
AstVarScope* thenVscp = nullptr;
|
|
|
|
|
AstVarScope* elseVscp = nullptr;
|
|
|
|
|
AstNode* const thenNodep = singleMeaningfulBranch(skipLeadingIgnorableStmt(ifp->thensp()));
|
|
|
|
|
UASSERT_OBJ(thenNodep, ifp, "Empty then-branch unexpectedly survived to FSM detection");
|
|
|
|
|
AstNode* const elseNodep = singleMeaningfulBranch(skipLeadingIgnorableStmt(ifp->elsesp()));
|
|
|
|
|
if (!elseNodep) return false;
|
|
|
|
|
if (!directConstStateAssignNode(thenNodep, thenVscp, thenValue)) return false;
|
|
|
|
|
if (!directConstStateAssignNode(elseNodep, elseVscp, elseValue)) return false;
|
|
|
|
|
if (thenVscp == stateVscp && elseVscp == stateVscp) return true;
|
|
|
|
|
if (thenVscp != elseVscp) return false;
|
|
|
|
|
AstNode* const followp = skipLeadingIgnorableStmt(ifp->nextp());
|
|
|
|
|
AstVarScope* finalStateVscp = nullptr;
|
|
|
|
|
AstVarScope* finalFromVscp = nullptr;
|
|
|
|
|
AstNode* const finalNodep = singleMeaningfulBranch(followp);
|
|
|
|
|
if (!finalNodep) return false;
|
|
|
|
|
if (!nodeStateVarAssign(finalNodep, finalStateVscp, finalFromVscp)) return false;
|
|
|
|
|
if (finalStateVscp != stateVscp) return false;
|
|
|
|
|
if (finalFromVscp != thenVscp) return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool directStateCondConstAssign(AstNode* stmtp, AstVarScope* stateVscp, int& thenValue,
|
|
|
|
|
int& elseValue) {
|
|
|
|
|
AstNodeAssign* const assp = directStateAssign(stmtp, stateVscp);
|
|
|
|
|
if (!assp) return false;
|
|
|
|
|
AstCond* const condp = VN_CAST(assp->rhsp(), Cond);
|
|
|
|
|
if (!condp) return false;
|
2026-05-07 12:53:19 +02:00
|
|
|
return constValueStatus(condp->thenp(), thenValue) == ConstValueStatus::OK
|
|
|
|
|
&& constValueStatus(condp->elsep(), elseValue) == ConstValueStatus::OK;
|
2026-04-30 13:22:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static AstNode* caseItemSupportedArcNode(AstCaseItem* itemp, AstVarScope* stateVscp,
|
|
|
|
|
bool inclCond) {
|
|
|
|
|
if (itemp->isDefault()) {
|
|
|
|
|
if (!inclCond) return nullptr;
|
|
|
|
|
}
|
|
|
|
|
AstNodeAssign* const assp = directStateAssign(itemp->stmtsp(), stateVscp);
|
|
|
|
|
if (assp) {
|
|
|
|
|
int toValue = 0;
|
2026-05-07 12:53:19 +02:00
|
|
|
if (constValueStatus(assp->rhsp(), toValue) == ConstValueStatus::OK) return assp;
|
2026-04-30 13:22:34 +02:00
|
|
|
}
|
|
|
|
|
int thenValue = 0;
|
|
|
|
|
int elseValue = 0;
|
|
|
|
|
if (directStateCondConstAssign(itemp->stmtsp(), stateVscp, thenValue, elseValue)) {
|
|
|
|
|
return assp;
|
|
|
|
|
}
|
|
|
|
|
if (ifStateConstAssign(itemp->stmtsp(), stateVscp, thenValue, elseValue)) {
|
|
|
|
|
return singleMeaningfulBranch(itemp->stmtsp());
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Combinational transition blocks are paired only through supported case
|
|
|
|
|
// items that assign to the recorded next-state variable.
|
|
|
|
|
static AstNode* caseSupportedTransitionNode(AstCase* casep, AstVarScope* stateVscp,
|
|
|
|
|
bool inclCond) {
|
|
|
|
|
for (AstCaseItem* itemp = casep->itemsp(); itemp;
|
|
|
|
|
itemp = VN_AS(itemp->nextp(), CaseItem)) {
|
|
|
|
|
if (AstNode* const nodep = caseItemSupportedArcNode(itemp, stateVscp, inclCond))
|
|
|
|
|
return nodep;
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 12:53:19 +02:00
|
|
|
// Prefer user labels in reports. Forced non-enum FSMs prepopulate synthetic
|
|
|
|
|
// labels, so all emitted arcs should already have a known label here.
|
|
|
|
|
static string labelForValue(const std::unordered_map<int, StateConstLabel>& labels,
|
|
|
|
|
int value) {
|
|
|
|
|
return labels.at(value).text;
|
2026-04-22 21:18:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The extractor only models constant-valued state transitions, and by the
|
|
|
|
|
// time detect runs those values have already been constant-folded.
|
2026-05-07 12:53:19 +02:00
|
|
|
enum class ConstValueStatus : uint8_t { OK, NOT_CONST, XZ, WIDE };
|
|
|
|
|
|
|
|
|
|
static ConstValueStatus constValueStatus(AstNodeExpr* exprp, int& value) {
|
|
|
|
|
const AstConst* const constp = VN_CAST(exprp, Const);
|
|
|
|
|
if (!constp) return ConstValueStatus::NOT_CONST;
|
|
|
|
|
const V3Number& num = constp->num();
|
|
|
|
|
if (num.isAnyXZ()) return ConstValueStatus::XZ;
|
|
|
|
|
// Some callers are still only probing candidate shapes, so wide constants
|
|
|
|
|
// should reject the candidate instead of reporting a V3Number error.
|
|
|
|
|
if (constp->width() > 32) return ConstValueStatus::WIDE;
|
|
|
|
|
value = constp->toSInt();
|
|
|
|
|
return ConstValueStatus::OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Enum-backed FSMs should only use values that were interned as known states.
|
|
|
|
|
// If a constant transition references some other encoding, warn and skip FSM
|
|
|
|
|
// instrumentation for that edge rather than silently dropping it or turning
|
|
|
|
|
// optional coverage into a hard compile failure.
|
|
|
|
|
static bool validateKnownStateValue(AstNode* nodep, const FsmStateSpace& stateSpace, int value,
|
|
|
|
|
const string& role) {
|
|
|
|
|
if (stateSpace.labels.find(value) != stateSpace.labels.end()) return true;
|
|
|
|
|
if (stateSpace.enumBacked) {
|
|
|
|
|
const string enumRole = role == "source" ? "case item value" : "assigned value";
|
|
|
|
|
nodep->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on enum state variable "
|
|
|
|
|
+ stateSpace.stateVarName + ": " + enumRole + " "
|
|
|
|
|
+ cvtToStr(value)
|
|
|
|
|
+ " is not present in the declared enum");
|
|
|
|
|
return false;
|
2026-04-22 21:18:59 +02:00
|
|
|
}
|
2026-05-07 12:53:19 +02:00
|
|
|
nodep->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on non-enum state variable "
|
|
|
|
|
+ stateSpace.stateVarName + ": " + role + " value "
|
|
|
|
|
+ cvtToStr(value)
|
|
|
|
|
+ " is not present in the inferred state space");
|
2026-04-22 21:18:59 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 12:53:19 +02:00
|
|
|
static StateConstLabel stateLabelForConst(AstConst* constp) {
|
|
|
|
|
const string name = constp->origParamName();
|
|
|
|
|
if (!name.empty()) return StateConstLabel{AstNode::prettyName(name), true, 0};
|
|
|
|
|
return StateConstLabel{constp->name(), false, 0};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void updateStateLabel(FsmStateSpace& stateSpace, int value,
|
|
|
|
|
const StateConstLabel& label) {
|
|
|
|
|
stateSpace.states.at(stateSpace.labels.at(value).stateIndex).first = label.text;
|
2026-04-22 21:18:59 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-30 13:22:34 +02:00
|
|
|
// Strict Phase 1 matcher for register processes: either a bare state
|
|
|
|
|
// commit, or a top-level reset guard whose else path is that commit.
|
|
|
|
|
static bool matchRegisterAlways(AstAlways* alwaysp, AstScope* scopep,
|
|
|
|
|
FsmRegisterCandidate& cand) {
|
|
|
|
|
if (!alwaysp->sentreep() || !alwaysp->sentreep()->hasEdge()) return false;
|
|
|
|
|
|
|
|
|
|
AstNode* const stmtsp = skipLeadingIgnorableStmt(alwaysp->stmtsp());
|
|
|
|
|
AstNode* const nodep = singleMeaningfulStmt(stmtsp);
|
|
|
|
|
if (!nodep) return false;
|
|
|
|
|
|
|
|
|
|
AstVarScope* stateVscp = nullptr;
|
|
|
|
|
AstVarScope* nextVscp = nullptr;
|
|
|
|
|
if (AstIf* const ifp = VN_CAST(nodep, If)) {
|
|
|
|
|
if (!ifp->elsesp() || !isSimpleResetCond(ifp->condp())) return false;
|
|
|
|
|
AstVarScope* resetStateVscp = nullptr;
|
|
|
|
|
const ResetAssignStatus resetStatus
|
|
|
|
|
= collectConstStateAssigns(ifp->thensp(), resetStateVscp, cand.resetArcs());
|
|
|
|
|
if (resetStatus == ResetAssignStatus::NONE) {
|
|
|
|
|
cand.resetArcs().clear();
|
|
|
|
|
int resetValue = 0;
|
|
|
|
|
AstNode* const thenNodep = singleMeaningfulBranch(ifp->thensp());
|
|
|
|
|
UASSERT_OBJ(thenNodep, ifp, "reset fallback requires a non-empty reset branch");
|
|
|
|
|
if (!directConstStateAssignNode(thenNodep, resetStateVscp, resetValue))
|
|
|
|
|
return false;
|
|
|
|
|
cand.resetArcs().emplace_back(resetValue, ifp->thensp());
|
|
|
|
|
} else if (resetStatus == ResetAssignStatus::MULTI_SAME_STATE) {
|
|
|
|
|
cand.resetArcs().clear();
|
|
|
|
|
}
|
|
|
|
|
AstNode* const elseNodep = singleMeaningfulBranch(ifp->elsesp());
|
|
|
|
|
UASSERT_OBJ(elseNodep, ifp, "register reset match requires a non-empty commit branch");
|
|
|
|
|
if (!nodeStateVarAssign(elseNodep, stateVscp, nextVscp)) return false;
|
|
|
|
|
if (resetStateVscp != stateVscp) return false;
|
|
|
|
|
cand.resetCond() = describeResetCond(ifp->condp());
|
|
|
|
|
cand.hasResetCond(cand.resetCond().varScopep != nullptr);
|
|
|
|
|
} else {
|
|
|
|
|
AstNodeExpr* resetCondp = nullptr;
|
|
|
|
|
int resetValue = 0;
|
|
|
|
|
if (AstNodeAssign* const assp
|
|
|
|
|
= directCondStateVarAssign(nodep, stateVscp, nextVscp, resetCondp, resetValue)) {
|
|
|
|
|
cand.resetArcs().emplace_back(resetValue, assp);
|
|
|
|
|
cand.resetCond() = describeResetCond(resetCondp);
|
|
|
|
|
cand.hasResetCond(cand.resetCond().varScopep != nullptr);
|
|
|
|
|
} else if (!nodeStateVarAssign(nodep, stateVscp, nextVscp)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cand.scopep(scopep);
|
|
|
|
|
cand.alwaysp(alwaysp);
|
|
|
|
|
cand.stateVscp(stateVscp);
|
|
|
|
|
cand.nextVscp(nextVscp);
|
|
|
|
|
cand.senses() = describeSenTree(alwaysp->sentreep());
|
|
|
|
|
cand.resetInclude(stateVscp->varp()->attrFsmResetArc());
|
|
|
|
|
cand.inclCond(stateVscp->varp()->attrFsmArcInclCond());
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 12:53:19 +02:00
|
|
|
// Helper: process a single condition expression and add it to the state space.
|
|
|
|
|
// Returns true on success, false if the state space is invalid.
|
|
|
|
|
static bool addCondToStateSpace(AstNodeExpr* condp, FsmStateSpace& stateSpace) {
|
|
|
|
|
int value = 0;
|
|
|
|
|
const ConstValueStatus status = constValueStatus(condp, value);
|
|
|
|
|
if (status != ConstValueStatus::OK) {
|
|
|
|
|
if (status == ConstValueStatus::XZ) {
|
|
|
|
|
condp->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on non-enum "
|
|
|
|
|
"state variable "
|
|
|
|
|
+ stateSpace.stateVarName
|
|
|
|
|
+ " with X/Z state encoding values");
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
AstConst* const constp = VN_AS(condp, Const);
|
|
|
|
|
const StateConstLabel label = stateLabelForConst(constp);
|
|
|
|
|
const auto labelIt = stateSpace.labels.find(value);
|
|
|
|
|
if (labelIt != stateSpace.labels.end()) {
|
|
|
|
|
StateConstLabel& existingLabel = labelIt->second;
|
|
|
|
|
if (existingLabel.text != label.text && existingLabel.fromParam && label.fromParam) {
|
|
|
|
|
condp->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on non-enum "
|
|
|
|
|
"state variable "
|
|
|
|
|
+ stateSpace.stateVarName
|
|
|
|
|
+ " with multiple labels for the same value "
|
|
|
|
|
+ cvtToStr(value) + ": " + existingLabel.text + " and "
|
|
|
|
|
+ label.text);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (!existingLabel.fromParam && label.fromParam) {
|
|
|
|
|
existingLabel.text = label.text;
|
|
|
|
|
existingLabel.fromParam = label.fromParam;
|
|
|
|
|
updateStateLabel(stateSpace, value, label);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
StateConstLabel storedLabel = label;
|
|
|
|
|
storedLabel.stateIndex = stateSpace.states.size();
|
|
|
|
|
stateSpace.states.emplace_back(label.text, value);
|
|
|
|
|
stateSpace.labels.emplace(value, storedLabel);
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 13:22:34 +02:00
|
|
|
// Build the Phase 1 state space from the tracked registered state
|
|
|
|
|
// variable, not from whichever signal the transition case happened to use.
|
2026-05-07 12:53:19 +02:00
|
|
|
static bool collectStateSpace(AstCase* casep, AstVarScope* stateVscp,
|
|
|
|
|
FsmStateSpace& stateSpace) {
|
2026-04-30 13:22:34 +02:00
|
|
|
AstVar* const stateVarp = stateVscp->varp();
|
|
|
|
|
AstEnumDType* enump = VN_CAST(unwrapEnumCandidate(stateVscp->dtypep()), EnumDType);
|
|
|
|
|
if (!enump) enump = VN_CAST(unwrapEnumCandidate(stateVarp->dtypep()), EnumDType);
|
|
|
|
|
const bool forced = stateVarp->attrFsmState();
|
2026-05-07 12:53:19 +02:00
|
|
|
stateSpace.stateVarName = stateVscp->prettyNameQ();
|
2026-04-30 13:22:34 +02:00
|
|
|
|
|
|
|
|
if (enump) {
|
|
|
|
|
if (stateVscp->width() > 32) {
|
2026-05-07 12:53:19 +02:00
|
|
|
casep->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on enum-typed state "
|
|
|
|
|
"variable "
|
|
|
|
|
+ stateSpace.stateVarName + " with width "
|
|
|
|
|
+ cvtToStr(stateVscp->width())
|
|
|
|
|
+ " wider than 32 bits");
|
2026-04-30 13:22:34 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
2026-05-07 12:53:19 +02:00
|
|
|
stateSpace.enumBacked = true;
|
2026-04-30 13:22:34 +02:00
|
|
|
for (AstEnumItem* itemp = enump->itemsp(); itemp;
|
|
|
|
|
itemp = VN_AS(itemp->nextp(), EnumItem)) {
|
|
|
|
|
const AstConst* const constp = VN_AS(itemp->valuep(), Const);
|
|
|
|
|
const int value = constp->toSInt();
|
2026-05-07 12:53:19 +02:00
|
|
|
const size_t stateIndex = stateSpace.states.size();
|
|
|
|
|
stateSpace.states.emplace_back(itemp->name(), value);
|
|
|
|
|
stateSpace.labels.emplace(value,
|
|
|
|
|
StateConstLabel{itemp->name(), false, stateIndex});
|
2026-04-30 13:22:34 +02:00
|
|
|
}
|
2026-05-07 12:53:19 +02:00
|
|
|
return stateSpace.states.size() >= 2;
|
2026-04-30 13:22:34 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-07 12:53:19 +02:00
|
|
|
if (forced) {
|
|
|
|
|
const int width = stateVarp->width();
|
|
|
|
|
if (width >= 31) return false;
|
|
|
|
|
const unsigned stateCount = 1U << width;
|
|
|
|
|
for (unsigned value = 0; value < stateCount; ++value) {
|
|
|
|
|
const string label = "S" + cvtToStr(value);
|
|
|
|
|
const size_t stateIndex = stateSpace.states.size();
|
|
|
|
|
stateSpace.states.emplace_back(label, static_cast<int>(value));
|
|
|
|
|
stateSpace.labels.emplace(static_cast<int>(value),
|
|
|
|
|
StateConstLabel{label, false, stateIndex});
|
|
|
|
|
}
|
|
|
|
|
return true;
|
2026-04-30 13:22:34 +02:00
|
|
|
}
|
2026-05-07 12:53:19 +02:00
|
|
|
|
|
|
|
|
if (stateVscp->width() > 32) {
|
|
|
|
|
casep->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on non-enum state "
|
|
|
|
|
"variable "
|
|
|
|
|
+ stateSpace.stateVarName + " with width "
|
|
|
|
|
+ cvtToStr(stateVscp->width()) + " wider than 32 bits");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (AstCaseItem* itemp = casep->itemsp(); itemp;
|
|
|
|
|
itemp = VN_AS(itemp->nextp(), CaseItem)) {
|
|
|
|
|
if (itemp->isDefault()) continue;
|
|
|
|
|
for (AstNodeExpr* condp = itemp->condsp(); condp;
|
|
|
|
|
condp = VN_AS(condp->nextp(), NodeExpr)) {
|
|
|
|
|
if (!addCondToStateSpace(condp, stateSpace)) return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return stateSpace.states.size() >= 2;
|
2026-04-30 13:22:34 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 21:18:59 +02:00
|
|
|
// Extract supported case-item transitions in one place so the conservative
|
|
|
|
|
// policy for direct and ternary forms stays consistent. The false exits in
|
|
|
|
|
// this helper are deliberate subset boundaries: they document shapes we do
|
|
|
|
|
// not yet model in this PR and that future FSM-detection work may widen.
|
|
|
|
|
static bool emitCaseItemArcs(FsmGraph& graph, AstCaseItem* itemp, AstVarScope* stateVscp,
|
2026-05-07 12:53:19 +02:00
|
|
|
const FsmStateSpace& stateSpace, bool inclCond) {
|
2026-04-22 21:18:59 +02:00
|
|
|
std::vector<std::pair<string, int>> froms;
|
|
|
|
|
if (itemp->isDefault()) {
|
|
|
|
|
if (!inclCond) return false;
|
|
|
|
|
froms.emplace_back("default", 0);
|
|
|
|
|
} else {
|
|
|
|
|
for (AstNodeExpr* condp = itemp->condsp(); condp;
|
2026-05-07 12:53:19 +02:00
|
|
|
condp = VN_AS(condp->nextp(), NodeExpr)) {
|
2026-04-22 21:18:59 +02:00
|
|
|
int value = 0;
|
2026-05-07 12:53:19 +02:00
|
|
|
if (constValueStatus(condp, value) != ConstValueStatus::OK) continue;
|
|
|
|
|
if (!validateKnownStateValue(condp, stateSpace, value, "source")) return true;
|
|
|
|
|
froms.emplace_back(labelForValue(stateSpace.labels, value), value);
|
2026-04-22 21:18:59 +02:00
|
|
|
}
|
|
|
|
|
if (froms.empty()) return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (AstNodeAssign* const assp = directStateAssign(itemp->stmtsp(), stateVscp)) {
|
|
|
|
|
int toValue = 0;
|
2026-05-07 12:53:19 +02:00
|
|
|
const ConstValueStatus status = constValueStatus(assp->rhsp(), toValue);
|
|
|
|
|
if (status == ConstValueStatus::OK) {
|
|
|
|
|
if (!validateKnownStateValue(assp, stateSpace, toValue, "target")) return true;
|
2026-04-22 21:18:59 +02:00
|
|
|
for (const std::pair<string, int>& from : froms) {
|
|
|
|
|
graph.addArc(from.second, toValue, false, false, itemp->isDefault(),
|
|
|
|
|
assp->fileline());
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-04-30 13:22:34 +02:00
|
|
|
}
|
2026-04-22 21:18:59 +02:00
|
|
|
|
2026-04-30 13:22:34 +02:00
|
|
|
int thenValue = 0;
|
|
|
|
|
int elseValue = 0;
|
|
|
|
|
if (directStateCondConstAssign(itemp->stmtsp(), stateVscp, thenValue, elseValue)
|
|
|
|
|
|| ifStateConstAssign(itemp->stmtsp(), stateVscp, thenValue, elseValue)) {
|
2026-05-07 12:53:19 +02:00
|
|
|
if (!validateKnownStateValue(itemp->stmtsp(), stateSpace, thenValue, "target"))
|
|
|
|
|
return true;
|
|
|
|
|
if (!validateKnownStateValue(itemp->stmtsp(), stateSpace, elseValue, "target"))
|
|
|
|
|
return true;
|
2026-04-30 13:22:34 +02:00
|
|
|
for (const int branchValue : {thenValue, elseValue}) {
|
|
|
|
|
for (const std::pair<string, int>& from : froms) {
|
|
|
|
|
graph.addArc(from.second, branchValue, false, true, itemp->isDefault(),
|
|
|
|
|
itemp->stmtsp()->fileline());
|
2026-04-22 21:18:59 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-30 13:22:34 +02:00
|
|
|
return true;
|
2026-04-22 21:18:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reset transitions are described separately because they live in the reset
|
|
|
|
|
// branch outside the steady-state case statement.
|
2026-04-30 13:22:34 +02:00
|
|
|
static void addResetArcs(FsmGraph& graph, const std::vector<FsmResetArcDesc>& resetArcs,
|
2026-05-07 12:53:19 +02:00
|
|
|
const FsmStateSpace& stateSpace) {
|
2026-04-30 13:22:34 +02:00
|
|
|
for (const FsmResetArcDesc& resetArc : resetArcs) {
|
2026-05-07 12:53:19 +02:00
|
|
|
if (!validateKnownStateValue(resetArc.nodep(), stateSpace, resetArc.toValue(),
|
|
|
|
|
"target"))
|
|
|
|
|
continue;
|
2026-04-30 13:22:34 +02:00
|
|
|
graph.addArc(0, resetArc.toValue(), true, false, false, resetArc.nodep()->fileline());
|
2026-04-22 21:18:59 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Turn one candidate case statement into the graph representation that the
|
|
|
|
|
// later lowering phase will consume directly, while reviewers can still
|
|
|
|
|
// inspect the extracted machine via DOT dumps.
|
2026-04-30 13:22:34 +02:00
|
|
|
void processCase(AstCase* casep, AstVarScope* assignVscp, const FsmRegisterCandidate& reg) {
|
|
|
|
|
UASSERT_OBJ(assignVscp, casep, "FSM case processing requires a non-null assignment var");
|
|
|
|
|
AstVarScope* const stateVscp = reg.stateVscp();
|
2026-05-07 12:53:19 +02:00
|
|
|
FsmStateSpace stateSpace;
|
|
|
|
|
if (!collectStateSpace(casep, stateVscp, stateSpace)) return;
|
2026-04-30 13:22:34 +02:00
|
|
|
DetectedFsm& entry = m_state.fsms()[stateVscp];
|
2026-04-22 21:18:59 +02:00
|
|
|
if (!entry.graphp) {
|
|
|
|
|
entry.graphp.reset(new FsmGraph{});
|
2026-04-30 13:22:34 +02:00
|
|
|
entry.graphp->scopep(reg.scopep());
|
|
|
|
|
entry.graphp->stateAlwaysp(reg.alwaysp());
|
2026-04-22 21:18:59 +02:00
|
|
|
entry.graphp->stateVarName(stateVscp->prettyName());
|
2026-04-30 13:22:34 +02:00
|
|
|
entry.graphp->stateVarInternalName(stateVscp->varp()->name());
|
2026-04-22 21:18:59 +02:00
|
|
|
entry.graphp->stateVarScopep(stateVscp);
|
2026-04-30 13:22:34 +02:00
|
|
|
entry.graphp->senses() = reg.senses();
|
|
|
|
|
entry.graphp->resetCond() = reg.resetCond();
|
|
|
|
|
entry.graphp->hasResetCond(reg.hasResetCond());
|
|
|
|
|
entry.graphp->resetInclude(reg.resetInclude());
|
|
|
|
|
entry.graphp->inclCond(reg.inclCond());
|
2026-04-22 21:18:59 +02:00
|
|
|
entry.graphp->fileline(casep->fileline());
|
2026-05-07 12:53:19 +02:00
|
|
|
for (const std::pair<string, int>& state : stateSpace.states) {
|
2026-04-22 21:18:59 +02:00
|
|
|
entry.graphp->addStateVertex(state.first, state.second);
|
|
|
|
|
}
|
2026-05-07 12:53:19 +02:00
|
|
|
addResetArcs(*entry.graphp, reg.resetArcs(), stateSpace);
|
2026-04-22 21:18:59 +02:00
|
|
|
}
|
2026-04-22 21:20:00 +02:00
|
|
|
for (AstCaseItem* itemp = casep->itemsp(); itemp;
|
|
|
|
|
itemp = VN_AS(itemp->nextp(), CaseItem)) {
|
2026-05-07 12:53:19 +02:00
|
|
|
emitCaseItemArcs(*entry.graphp, itemp, assignVscp, stateSpace,
|
|
|
|
|
entry.graphp->inclCond());
|
2026-04-22 21:18:59 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the first supported FSM candidate in a clocked always block, warn on
|
|
|
|
|
// additional candidates, and attach reset arcs when present. Candidate
|
|
|
|
|
// filtering stays narrow on purpose: we prefer to skip ambiguous shapes now
|
|
|
|
|
// and expand detection in a later PR rather than over-infer coverage from
|
|
|
|
|
// forms we do not yet model confidently.
|
2026-04-30 13:22:34 +02:00
|
|
|
void processOneBlockAlways(AstAlways* alwaysp) {
|
|
|
|
|
const RegisterAlwaysAnalyzer analyzer{m_scopep};
|
|
|
|
|
if (!alwaysp->sentreep() || !alwaysp->sentreep()->hasEdge()) return;
|
|
|
|
|
const std::vector<std::pair<AstCase*, AstNodeExpr*>> candidates
|
|
|
|
|
= analyzer.oneBlockCandidates(alwaysp);
|
2026-04-22 21:18:59 +02:00
|
|
|
if (candidates.empty()) return;
|
|
|
|
|
|
2026-04-30 13:22:34 +02:00
|
|
|
FsmCaseCandidate firstCand;
|
2026-04-22 21:18:59 +02:00
|
|
|
for (const std::pair<AstCase*, AstNodeExpr*>& cand : candidates) {
|
|
|
|
|
AstVarRef* const selp = VN_CAST(cand.first->exprp(), VarRef);
|
|
|
|
|
AstVarScope* const vscp = selp ? selp->varScopep() : nullptr;
|
|
|
|
|
if (!vscp) continue;
|
2026-04-30 13:22:34 +02:00
|
|
|
if (!firstCand.stateVscp) {
|
|
|
|
|
firstCand.warnNodep = cand.first;
|
|
|
|
|
firstCand.stateVscp = vscp;
|
|
|
|
|
FsmRegisterCandidate reg;
|
|
|
|
|
analyzer.buildOneBlockCandidate(alwaysp, vscp, cand.second, reg);
|
|
|
|
|
processCase(cand.first, vscp, reg);
|
|
|
|
|
} else if (vscp != firstCand.stateVscp) {
|
2026-04-22 21:18:59 +02:00
|
|
|
cand.first->v3warn(FSMMULTI,
|
|
|
|
|
"FSM coverage: multiple enum-typed case statements found in "
|
|
|
|
|
"the same always block. Only the first candidate will be "
|
2026-04-30 13:22:34 +02:00
|
|
|
"instrumented."
|
|
|
|
|
<< candidateConflictContext(cand.first, firstCand));
|
2026-04-22 21:18:59 +02:00
|
|
|
} else {
|
2026-04-22 21:20:00 +02:00
|
|
|
cand.first->v3warn(COVERIGN,
|
|
|
|
|
"Ignoring unsupported: FSM coverage on multiple supported case "
|
|
|
|
|
"statements found in the same always block. Only the first "
|
2026-04-30 13:22:34 +02:00
|
|
|
"candidate will be instrumented."
|
|
|
|
|
<< candidateConflictContext(cand.first, firstCand));
|
2026-04-22 21:18:59 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-30 13:22:34 +02:00
|
|
|
}
|
2026-04-22 21:18:59 +02:00
|
|
|
|
2026-04-30 13:22:34 +02:00
|
|
|
// Phase 1 two-process pairing scans combinational always blocks only after
|
|
|
|
|
// all strict register candidates have been collected, so source order does
|
|
|
|
|
// not matter.
|
|
|
|
|
static void warnComboSameAlways(AstNode* warnNodep, const FsmCaseCandidate& firstCand) {
|
|
|
|
|
warnNodep->v3warn(FSMMULTI,
|
|
|
|
|
"FSM coverage: multiple supported transition candidates found in "
|
|
|
|
|
"the same combinational always block. Only the first candidate "
|
|
|
|
|
"will be instrumented."
|
|
|
|
|
<< candidateConflictContext(warnNodep, firstCand));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void processComboAlways(const FsmComboAlways& combo) {
|
|
|
|
|
const ComboAlwaysAnalyzer analyzer{m_registerCandidates};
|
|
|
|
|
AstNode* const stmtsp = skipLeadingIgnorableStmt(combo.alwaysp()->stmtsp());
|
|
|
|
|
FsmCaseCandidate firstCand;
|
|
|
|
|
for (AstNode* nodep = stmtsp; nodep; nodep = nodep->nextp()) {
|
|
|
|
|
AstCase* const casep = VN_CAST(nodep, Case);
|
|
|
|
|
if (!casep) continue;
|
|
|
|
|
const ComboAlwaysAnalyzer::ComboMatch match = analyzer.matchCase(stmtsp, casep);
|
|
|
|
|
const FsmRegisterCandidate* const matchedp = match.matchedp;
|
|
|
|
|
AstNode* const matchedWarnNodep = match.warnNodep;
|
|
|
|
|
if (!matchedp) continue;
|
|
|
|
|
if (!firstCand.stateVscp) {
|
|
|
|
|
const auto insertPair = m_comboPaired.emplace(
|
2026-04-30 13:23:36 +02:00
|
|
|
matchedp->stateVscp(),
|
|
|
|
|
FsmCaseCandidate{matchedWarnNodep,
|
|
|
|
|
const_cast<AstVarScope*>(matchedp->stateVscp())});
|
2026-04-30 13:22:34 +02:00
|
|
|
if (!insertPair.second) {
|
|
|
|
|
matchedWarnNodep->v3warn(
|
2026-04-30 13:23:36 +02:00
|
|
|
FSMMULTI, "FSM coverage: multiple supported transition candidates found "
|
|
|
|
|
"for the same FSM in combinational always blocks. Only the "
|
|
|
|
|
"first candidate will be instrumented."
|
|
|
|
|
<< candidateConflictContext(matchedWarnNodep,
|
|
|
|
|
insertPair.first->second));
|
2026-04-30 13:22:34 +02:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
firstCand.warnNodep = matchedWarnNodep;
|
|
|
|
|
firstCand.stateVscp = const_cast<AstVarScope*>(matchedp->stateVscp());
|
|
|
|
|
processCase(casep, matchedp->nextVscp(), *matchedp);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (matchedp->stateVscp() != firstCand.stateVscp) {
|
|
|
|
|
warnComboSameAlways(matchedWarnNodep, firstCand);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
matchedWarnNodep->v3warn(COVERIGN,
|
|
|
|
|
"Ignoring unsupported: FSM coverage on multiple "
|
|
|
|
|
"supported case statements found in the same "
|
|
|
|
|
"combinational always block. Only the first "
|
|
|
|
|
"candidate will be instrumented."
|
2026-04-30 13:23:36 +02:00
|
|
|
<< candidateConflictContext(matchedWarnNodep, firstCand));
|
2026-04-22 21:18:59 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Track the current scope so each detected FSM records the module/scope
|
|
|
|
|
// where instrumentation must later be inserted.
|
|
|
|
|
void visit(AstScope* nodep) override {
|
|
|
|
|
VL_RESTORER(m_scopep);
|
|
|
|
|
m_scopep = nodep;
|
|
|
|
|
iterateChildren(nodep);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 13:22:34 +02:00
|
|
|
// Collect one-block FSMs immediately, strict register candidates for later
|
|
|
|
|
// pairing, and combinational processes for the second-stage transition
|
|
|
|
|
// scan.
|
|
|
|
|
void visit(AstAlways* nodep) override {
|
|
|
|
|
processOneBlockAlways(nodep);
|
|
|
|
|
const RegisterAlwaysAnalyzer analyzer{m_scopep};
|
|
|
|
|
FsmRegisterCandidate reg;
|
|
|
|
|
if (analyzer.matchRegisterCandidate(nodep, reg)) {
|
|
|
|
|
m_registerCandidates.emplace(reg.stateVscp(), reg);
|
|
|
|
|
}
|
|
|
|
|
if (nodep->keyword() == VAlwaysKwd::ALWAYS_COMB) {
|
|
|
|
|
m_comboAlwayss.emplace_back(m_scopep, nodep);
|
|
|
|
|
} else if (nodep->keyword() == VAlwaysKwd::ALWAYS) {
|
|
|
|
|
if (!nodep->sentreep() || isPlainComboSentree(nodep->sentreep())) {
|
|
|
|
|
m_comboAlwayss.emplace_back(m_scopep, nodep);
|
|
|
|
|
} else {
|
|
|
|
|
m_nonComboAlwayss.emplace_back(m_scopep, nodep);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-22 21:18:59 +02:00
|
|
|
|
|
|
|
|
// Continue the walk through the rest of the design hierarchy.
|
|
|
|
|
void visit(AstNode* nodep) override { iterateChildren(nodep); }
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
// CONSTRUCTORS
|
|
|
|
|
// Collect all FSM graphs into the shared local state before the lowering
|
|
|
|
|
// phase starts mutating the AST with coverage machinery.
|
|
|
|
|
FsmDetectVisitor(FsmState& state, AstNetlist* rootp)
|
|
|
|
|
: m_state{state} {
|
|
|
|
|
iterate(rootp);
|
2026-04-30 13:22:34 +02:00
|
|
|
for (const FsmComboAlways& combo : m_comboAlwayss) processComboAlways(combo);
|
|
|
|
|
for (const FsmComboAlways& combo : m_nonComboAlwayss) warnUnsupportedComboAlways(combo);
|
2026-04-22 21:18:59 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Lower the completed FSM graphs into the concrete coverage declarations,
|
|
|
|
|
// previous-state tracking, and pre/post-triggered instrumentation that the
|
|
|
|
|
// runtime uses to record state and transition coverage.
|
|
|
|
|
class FsmLowerVisitor final {
|
|
|
|
|
// STATE - across all visitors
|
|
|
|
|
const FsmState& m_state;
|
|
|
|
|
|
|
|
|
|
// METHODS
|
|
|
|
|
// Rebuild a state-typed constant using the tracked state variable
|
|
|
|
|
// width/sign so emitted comparisons match the original representation.
|
|
|
|
|
static AstConst* makeStateConst(FileLine* flp, AstVarScope* vscp, int value) {
|
|
|
|
|
V3Number num{flp, vscp->width(), static_cast<uint32_t>(value)};
|
|
|
|
|
num.isSigned(vscp->dtypep()->isSigned());
|
|
|
|
|
return new AstConst{flp, num};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build guards incrementally without forcing callers to special-case the
|
|
|
|
|
// first predicate; this keeps emitted state/arc conditions readable.
|
|
|
|
|
static AstNodeExpr* andExpr(FileLine* flp, AstNodeExpr* lhsp, AstNodeExpr* rhsp) {
|
|
|
|
|
if (!lhsp) return rhsp;
|
|
|
|
|
return new AstLogAnd{flp, lhsp, rhsp};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static AstNodeExpr* buildResetCond(FileLine* flp, AstVarScope* resetVscp,
|
|
|
|
|
const FsmResetCondDesc&) {
|
|
|
|
|
return new AstVarRef{flp, resetVscp, VAccess::READ};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rebuild the original event control from the saved sense description so
|
|
|
|
|
// post-state coverage sampling runs on the same triggering edges.
|
2026-04-22 21:20:00 +02:00
|
|
|
static AstSenTree* buildSenTree(FileLine* flp, const std::vector<FsmSenDesc>& senses) {
|
2026-04-22 21:18:59 +02:00
|
|
|
AstSenTree* const sentreep = new AstSenTree{flp, nullptr};
|
|
|
|
|
for (const FsmSenDesc& sense : senses) {
|
2026-04-22 21:20:00 +02:00
|
|
|
AstSenItem* const senItemp
|
|
|
|
|
= new AstSenItem{flp, VEdgeType{sense.edgeType},
|
|
|
|
|
new AstVarRef{flp, sense.varScopep, VAccess::READ}};
|
2026-04-22 21:18:59 +02:00
|
|
|
sentreep->addSensesp(senItemp);
|
|
|
|
|
}
|
|
|
|
|
return sentreep;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Lower one fully detected FSM graph into the concrete coverage machinery
|
|
|
|
|
// used by generated models: declarations, previous-state tracking, and the
|
|
|
|
|
// pre/post-triggered increment logic for states and arcs.
|
|
|
|
|
void buildOne(const FsmGraph& graph) {
|
2026-04-30 13:22:34 +02:00
|
|
|
UINFO(1, "buildOne lowering FSM " << graph.stateVarName()
|
|
|
|
|
<< " vertices=" << graph.vertices().size() << endl);
|
|
|
|
|
AstAlways* const alwaysp = graph.stateAlwaysp();
|
2026-04-22 21:18:59 +02:00
|
|
|
AstScope* const scopep = graph.scopep();
|
|
|
|
|
AstVarScope* const stateVscp = graph.stateVarScopep();
|
|
|
|
|
FileLine* const flp = graph.fileline();
|
|
|
|
|
AstNodeModule* const modp = scopep->modp();
|
2026-04-22 21:20:00 +02:00
|
|
|
AstNodeDType* const prevDTypep = scopep->findLogicDType(
|
|
|
|
|
stateVscp->width(), stateVscp->width(), stateVscp->dtypep()->numeric());
|
2026-04-22 21:18:59 +02:00
|
|
|
AstVarScope* const prevVscp
|
|
|
|
|
= scopep->createTemp("__Vfsmcov_prev__" + stateVscp->varp()->shortName(), prevDTypep);
|
|
|
|
|
// The saved previous-state temp crosses the scheduler's pre/post split
|
|
|
|
|
// in the same way as Verilator's built-in NBA shadow variables, so keep
|
|
|
|
|
// both vars marked as post-life participants for stable MT ordering.
|
|
|
|
|
stateVscp->optimizeLifePost(true);
|
|
|
|
|
prevVscp->optimizeLifePost(true);
|
|
|
|
|
|
|
|
|
|
AstActive* const initActivep
|
|
|
|
|
= new AstActive{flp, "fsm-coverage-init",
|
|
|
|
|
new AstSenTree{flp, new AstSenItem{flp, AstSenItem::Initial{}}}};
|
|
|
|
|
initActivep->senTreeStorep(initActivep->sentreep());
|
|
|
|
|
// Seed the previous-state temp during initialization so the first
|
|
|
|
|
// clock edge compares against a defined state value.
|
|
|
|
|
initActivep->addStmtsp(new AstInitialStatic{
|
|
|
|
|
flp, new AstAssign{flp, new AstVarRef{flp, prevVscp, VAccess::WRITE},
|
|
|
|
|
new AstVarRef{flp, stateVscp, VAccess::READ}}});
|
|
|
|
|
scopep->addBlocksp(initActivep);
|
|
|
|
|
|
|
|
|
|
AstAlwaysPost* const covPostp = new AstAlwaysPost{flp};
|
|
|
|
|
// Save the previous state as plain sequential logic at the front of
|
|
|
|
|
// the original always_ff body, then evaluate coverage in post logic
|
|
|
|
|
// after the delayed state update commits. This avoids a scheduler race
|
|
|
|
|
// between a separate AstAlwaysPre task and the real state commit.
|
|
|
|
|
AstNode* const bodysp = alwaysp->stmtsp()->unlinkFrBackWithNext();
|
|
|
|
|
alwaysp->addStmtsp(new AstAssign{flp, new AstVarRef{flp, prevVscp, VAccess::WRITE},
|
|
|
|
|
new AstVarRef{flp, stateVscp, VAccess::READ}});
|
|
|
|
|
alwaysp->addStmtsp(bodysp);
|
|
|
|
|
|
|
|
|
|
for (const V3GraphVertex& vtx : graph.vertices()) {
|
|
|
|
|
const FsmVertex* const vertexp = vtx.as<FsmVertex>();
|
|
|
|
|
if (!vertexp->isState()) continue;
|
|
|
|
|
const FsmStateVertex* const statep = vtx.as<FsmStateVertex>();
|
|
|
|
|
// State coverage fires when the FSM enters a state from any other
|
|
|
|
|
// value, so repeated self-holds do not count as new entries.
|
2026-04-22 21:20:00 +02:00
|
|
|
AstCoverOtherDecl* const declp
|
|
|
|
|
= new AstCoverOtherDecl{flp,
|
|
|
|
|
"v_fsm_state/" + modp->prettyName(),
|
|
|
|
|
graph.stateVarName() + "::" + statep->label(),
|
|
|
|
|
"",
|
|
|
|
|
0,
|
|
|
|
|
graph.stateVarName(),
|
|
|
|
|
"",
|
|
|
|
|
statep->label()};
|
2026-04-22 21:18:59 +02:00
|
|
|
declp->hier(scopep->prettyName());
|
|
|
|
|
modp->addStmtsp(declp);
|
|
|
|
|
AstNodeExpr* const guardp
|
|
|
|
|
= andExpr(flp,
|
|
|
|
|
new AstNeq{flp, new AstVarRef{flp, prevVscp, VAccess::READ},
|
|
|
|
|
makeStateConst(flp, prevVscp, statep->value())},
|
|
|
|
|
new AstEq{flp, new AstVarRef{flp, stateVscp, VAccess::READ},
|
|
|
|
|
makeStateConst(flp, stateVscp, statep->value())});
|
|
|
|
|
covPostp->addStmtsp(new AstIf{flp, guardp, new AstCoverInc{flp, declp}});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const V3GraphVertex& vtx : graph.vertices()) {
|
|
|
|
|
const FsmVertex* const fromVertexp = vtx.as<FsmVertex>();
|
|
|
|
|
for (const V3GraphEdge& edge : fromVertexp->outEdges()) {
|
|
|
|
|
const FsmArcEdge* const arcp = edge.as<FsmArcEdge>();
|
|
|
|
|
const FsmStateVertex* const toStatep = arcp->top()->as<FsmStateVertex>();
|
|
|
|
|
// Arc coverage mirrors the extracted graph exactly, including
|
|
|
|
|
// reset and synthetic-default sources, so reports match the
|
|
|
|
|
// reviewer-visible graph dump and the user-visible annotation.
|
|
|
|
|
const string resetTag
|
2026-04-22 21:20:00 +02:00
|
|
|
= arcp->isReset() ? (graph.resetInclude() ? "[reset_include]" : "[reset]")
|
|
|
|
|
: "";
|
|
|
|
|
const string fsmTag = arcp->isReset()
|
|
|
|
|
? (graph.resetInclude() ? "reset_include" : "reset")
|
|
|
|
|
: arcp->isDefault() ? "default"
|
|
|
|
|
: "";
|
|
|
|
|
AstCoverOtherDecl* const declp
|
|
|
|
|
= new AstCoverOtherDecl{flp,
|
|
|
|
|
"v_fsm_arc/" + modp->prettyName(),
|
|
|
|
|
graph.stateVarName() + "::" + fromVertexp->label()
|
|
|
|
|
+ "->" + toStatep->label() + resetTag,
|
|
|
|
|
"",
|
|
|
|
|
0,
|
|
|
|
|
graph.stateVarName(),
|
|
|
|
|
fromVertexp->label(),
|
|
|
|
|
toStatep->label(),
|
|
|
|
|
fsmTag};
|
2026-04-22 21:18:59 +02:00
|
|
|
declp->hier(scopep->prettyName());
|
|
|
|
|
modp->addStmtsp(declp);
|
|
|
|
|
AstNodeExpr* guardp = nullptr;
|
|
|
|
|
if (fromVertexp->isResetAny()) {
|
|
|
|
|
// Reset arcs are modeled as pseudo-source edges in the
|
|
|
|
|
// graph, then reconstructed here into the original simple
|
|
|
|
|
// reset predicate combined with the destination state.
|
|
|
|
|
guardp = buildResetCond(flp, graph.resetCond().varScopep, graph.resetCond());
|
|
|
|
|
guardp = andExpr(flp, guardp,
|
|
|
|
|
new AstEq{flp, new AstVarRef{flp, stateVscp, VAccess::READ},
|
|
|
|
|
makeStateConst(flp, stateVscp, toStatep->value())});
|
|
|
|
|
} else if (fromVertexp->isDefaultAny()) {
|
|
|
|
|
// Synthetic default arcs mean "none of the explicit
|
|
|
|
|
// source states matched", so rebuild that as a conjunction
|
|
|
|
|
// of previous-state != known-state tests.
|
|
|
|
|
for (const V3GraphVertex& stateVtx : graph.vertices()) {
|
|
|
|
|
const FsmVertex* const stateVertexp = stateVtx.as<FsmVertex>();
|
|
|
|
|
if (!stateVertexp->isState()) continue;
|
|
|
|
|
guardp = andExpr(
|
|
|
|
|
flp, guardp,
|
|
|
|
|
new AstNeq{flp, new AstVarRef{flp, prevVscp, VAccess::READ},
|
|
|
|
|
makeStateConst(flp, prevVscp, stateVertexp->value())});
|
|
|
|
|
}
|
|
|
|
|
guardp = andExpr(flp, guardp,
|
|
|
|
|
new AstEq{flp, new AstVarRef{flp, stateVscp, VAccess::READ},
|
|
|
|
|
makeStateConst(flp, stateVscp, toStatep->value())});
|
|
|
|
|
} else {
|
2026-04-22 21:20:00 +02:00
|
|
|
guardp
|
|
|
|
|
= andExpr(flp,
|
|
|
|
|
new AstEq{flp, new AstVarRef{flp, prevVscp, VAccess::READ},
|
|
|
|
|
makeStateConst(flp, prevVscp, fromVertexp->value())},
|
|
|
|
|
new AstEq{flp, new AstVarRef{flp, stateVscp, VAccess::READ},
|
|
|
|
|
makeStateConst(flp, stateVscp, toStatep->value())});
|
2026-04-22 21:18:59 +02:00
|
|
|
}
|
|
|
|
|
covPostp->addStmtsp(new AstIf{flp, guardp, new AstCoverInc{flp, declp}});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AstSenTree* const sentreep = buildSenTree(flp, graph.senses());
|
|
|
|
|
AstActive* const activep = new AstActive{flp, "fsm-coverage", sentreep};
|
|
|
|
|
activep->senTreeStorep(sentreep);
|
|
|
|
|
scopep->addBlocksp(activep);
|
|
|
|
|
activep->addStmtsp(covPostp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
// CONSTRUCTORS
|
|
|
|
|
// Lower every detected FSM graph from the shared local state into
|
|
|
|
|
// concrete coverage instrumentation while the saved scoped pointers are
|
|
|
|
|
// still valid in the same pass.
|
|
|
|
|
explicit FsmLowerVisitor(const FsmState& state)
|
|
|
|
|
: m_state{state} {
|
2026-04-30 13:22:34 +02:00
|
|
|
for (const auto& it : m_state.fsms()) { buildOne(*it.second.graphp); }
|
2026-04-22 21:18:59 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
void V3FsmDetect::detect(AstNetlist* rootp) {
|
|
|
|
|
UINFO(2, __FUNCTION__ << ":");
|
|
|
|
|
FsmState state;
|
|
|
|
|
// Phase 1: recover each supported FSM into a complete graph while the
|
|
|
|
|
// original clocked/case structure is still easy to recognize.
|
|
|
|
|
FsmDetectVisitor detect{state, rootp};
|
|
|
|
|
if (dumpGraphLevel() >= 6) {
|
|
|
|
|
size_t index = 0;
|
2026-04-30 13:22:34 +02:00
|
|
|
for (const auto& it : state.fsms()) {
|
2026-04-22 21:18:59 +02:00
|
|
|
it.second.graphp->dumpDotFilePrefixed(it.second.graphp->dumpTag(index++));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Phase 2: lower the completed in-memory graph state immediately, without
|
|
|
|
|
// crossing into another pass owner or serializing through AST placeholders.
|
|
|
|
|
{ FsmLowerVisitor lower{state}; }
|
|
|
|
|
V3Global::dumpCheckGlobalTree("fsm-detect", 0, dumpTreeEitherLevel() >= 3);
|
|
|
|
|
}
|