diff --git a/include/verilated_covergroup.cpp b/include/verilated_covergroup.cpp index 258db6940..50ee42bc4 100644 --- a/include/verilated_covergroup.cpp +++ b/include/verilated_covergroup.cpp @@ -25,6 +25,9 @@ #include "verilated.h" +#include +#include + // This file is compiled whenever covergroups are used, with or without // "verilator --coverage" (see V3Global::verilatedCppFiles). Bin counts are // owned by the covergroup instance nodes in the VerilatedContext's registry, so @@ -38,6 +41,491 @@ #include "verilated_cov.h" #endif +struct VlCoverpoint::ValueData final { + // CONSTANTS + static constexpr uint32_t QUERY_WORK_LIMIT = 1U << 20; // Maximum graph steps per query + static constexpr uint32_t QUERY_DEPTH_LIMIT = 1024; // Max width for recursive queries + + // TYPES + // A value's words, held inline up to INLINE_WORDS so that common widths do not allocate + class Value final { + static constexpr uint32_t INLINE_WORDS = 2; // Words stored without an allocation + uint32_t m_size = 0; // Number of words + EData m_inline[INLINE_WORDS] = {0, 0}; // Words of a value up to INLINE_WORDS wide + std::vector m_heap; // Words of a wider value + + public: + Value() = default; + Value(const EData* beginp, const EData* endp) + : m_size{static_cast(endp - beginp)} { + if (m_size <= INLINE_WORDS) { + std::copy(beginp, endp, m_inline); + } else { + m_heap.assign(beginp, endp); + } + } + EData* data() { return m_size <= INLINE_WORDS ? m_inline : m_heap.data(); } + const EData* data() const { return m_size <= INLINE_WORDS ? m_inline : m_heap.data(); } + bool empty() const { return !m_size; } + void clear() { + m_size = 0; + m_heap.clear(); + } + EData& operator[](uint32_t i) { return data()[i]; } + const EData& operator[](uint32_t i) const { return data()[i]; } + EData& back() { return data()[m_size - 1]; } + EData* begin() { return data(); } + EData* end() { return data() + m_size; } + const EData* begin() const { return data(); } + const EData* end() const { return data() + m_size; } + bool operator==(const Value& other) const { + return std::equal(begin(), end(), other.begin(), other.end()); + } + }; + struct Range final { + Value m_lo; // Inclusive lower bound and fixed-bit values for wildcard patterns + Value m_hi; // Inclusive upper bound in coverpoint value order + Value m_mask; // Significant wildcard bits; empty for an ordinary interval + }; + struct Values final { + std::vector m_ranges; // Source intervals and patterns associated with this bin + bool m_transition = false; // State-value exclusions must not alter this transition bin + }; + // Outcome of searching a range for a value outside every exclusion + enum class Search : uint8_t { EMPTY, VALUE, WORK_LIMIT, DEPTH_LIMIT }; + class Query; + + // MEMBERS + const uint32_t m_bits; // Width of the coverpoint's effective integral type + const uint32_t m_words; // EData words required to store one value + const bool m_isSigned; // Use signed ordering when comparing values + bool m_frozen = false; // Construction-time value metadata has been finalized + std::vector m_values; // Values by declared bin, released once crosses are built + std::vector m_exclusions; // Normalized state ignore/illegal ranges and patterns + uint32_t m_regularExclusions = 0; // Length of the merged interval prefix in m_exclusions + std::vector m_reported; // Declared bins that have values, in declaration order + + ValueData(uint32_t bits, bool isSigned, uint32_t bins) + : m_bits{bits} + , m_words{VL_WORDS_I(bits)} + , m_isSigned{isSigned} + , m_values{bins} { + assert(m_bits); + } + static WDataInP view(const Value& value) { return WDataInP::external(value.data()); } + Value read(WDataInP valuep) const { + Value result(valuep.datap(), valuep.datap() + m_words); + return result; + } + // Operands have already been cleaned to m_bits. + bool less(WDataInP lhs, WDataInP rhs) const { + if (m_isSigned) { + const EData leftSign = VL_SIGN_E(m_bits, lhs[m_words - 1]); + const EData rightSign = VL_SIGN_E(m_bits, rhs[m_words - 1]); + if (leftSign != rightSign) return leftSign; + } + for (uint32_t i = m_words; i > 0; --i) { + const EData left = lhs[i - 1]; + const EData right = rhs[i - 1]; + if (left != right) return left < right; + } + return false; + } + bool less(const Value& lhs, const Value& rhs) const { return less(view(lhs), view(rhs)); } + bool less(WDataInP lhs, const Value& rhs) const { return less(lhs, view(rhs)); } + bool less(const Value& lhs, WDataInP rhs) const { return less(view(lhs), rhs); } + void increment(Value& value) const { + for (EData& word : value) { + if (++word) break; + } + value.back() &= VL_MASK_E(m_bits); + } + bool contains(const Range& range, WDataInP value) const { + if (less(value, range.m_lo) || less(range.m_hi, value)) return false; + if (!range.m_mask.empty()) { + EData mismatch = 0; + for (uint32_t i = 0; i < m_words; ++i) { + mismatch |= (value[i] & range.m_mask[i]) ^ (range.m_lo[i] & range.m_mask[i]); + } + return mismatch == 0; + } + return true; + } + const Range* interval(const std::vector& ranges, uint32_t count, WDataInP value) const { + auto it = std::upper_bound( + ranges.begin(), ranges.begin() + count, value, + [&](WDataInP candidate, const Range& range) { return less(candidate, range.m_lo); }); + if (it == ranges.begin()) return nullptr; + --it; + return contains(*it, value) ? &*it : nullptr; + } + uint32_t normalize(std::vector& ranges) const { + // Most bins have one ordered range, which needs no sorting or merging. + if (ranges.size() == 1 && !less(ranges[0].m_hi, ranges[0].m_lo)) { + return ranges[0].m_mask.empty() ? 1 : 0; + } + const auto middle = std::stable_partition( + ranges.begin(), ranges.end(), [](const Range& range) { return range.m_mask.empty(); }); + std::sort(ranges.begin(), middle, + [&](const Range& lhs, const Range& rhs) { return less(lhs.m_lo, rhs.m_lo); }); + std::vector merged; + for (auto it = ranges.begin(); it != middle; ++it) { + if (less(it->m_hi, it->m_lo)) continue; + if (!merged.empty()) { + Value adjacent = merged.back().m_hi; + increment(adjacent); + if (!less(merged.back().m_hi, it->m_lo) || adjacent == it->m_lo) { + if (less(merged.back().m_hi, it->m_hi)) merged.back().m_hi = it->m_hi; + continue; + } + } + merged.push_back(std::move(*it)); + } + const uint32_t count = static_cast(merged.size()); + merged.insert(merged.end(), std::make_move_iterator(middle), + std::make_move_iterator(ranges.end())); + ranges = std::move(merged); + return count; + } + bool excluded(WDataInP value) const { + return interval(m_exclusions, m_regularExclusions, value) + || std::any_of(m_exclusions.begin() + m_regularExclusions, m_exclusions.end(), + [&](const Range& range) { return contains(range, value); }); + } + // Value bits, unlike wildcard mask bits, flip the sign bit for signed ordering. + bool orderBit(const Value& value, uint32_t bit) const { + return (VL_BITISSET_W(value, bit) != 0) ^ (m_isSigned && bit == m_bits - 1); + } + void setOrderBit(Value& value, uint32_t bit, bool ordered) const { + VL_ASSIGNBIT_II(bit, value[VL_BITWORD_E(bit)], + ordered ^ (m_isSigned && bit == m_bits - 1)); + } + bool patternAtLeast(const Range& range, const Value& lower, Value& result) const { + if (range.m_mask.empty()) { + result = lower; + return true; + } + result = range.m_lo; + int32_t carry = -1; + bool greater = false; + for (uint32_t pos = m_bits; pos > 0;) { + const uint32_t bit = --pos; + const bool fixed = VL_BITISSET_W(range.m_mask, bit); + const bool low = orderBit(lower, bit); + const bool chosen = fixed ? orderBit(range.m_lo, bit) : greater ? false : low; + if (!greater && fixed && !chosen && low) { + if (carry < 0) return false; + setOrderBit(result, static_cast(carry), true); + for (uint32_t tail = 0; tail < static_cast(carry); ++tail) { + const uint32_t w = VL_BITWORD_E(tail); + VL_ASSIGNBIT_II(tail, result[w], + VL_BITISSET_E(range.m_lo[w] & range.m_mask[w], tail) != 0); + } + return true; + } + if (!greater && !fixed && !chosen) carry = static_cast(bit); + if (chosen != low) greater |= chosen && !low; + setOrderBit(result, bit, chosen); + } + return true; + } + bool clip(Range& range, const Value& lo, const Value& hi) const { + const Value lower = less(lo, range.m_lo) ? range.m_lo : lo; + const Value upper = less(range.m_hi, hi) ? range.m_hi : hi; + Value first; + if (less(upper, lower) || !patternAtLeast(range, lower, first) || less(upper, first)) { + return false; + } + range.m_lo = std::move(first); + range.m_hi = upper; + return true; + } + bool pattern(WDataInP valuep, WDataInP maskp, WDataInP lop, WDataInP hip, + Range& result) const { + result = {read(valuep), read(valuep), read(maskp)}; + for (uint32_t i = 0; i < m_words; ++i) { + result.m_lo[i] &= result.m_mask[i]; + result.m_hi[i] |= ~result.m_mask[i]; + } + result.m_hi.back() &= VL_MASK_E(m_bits); + if (m_isSigned && !VL_SIGN_E(m_bits, result.m_mask.back())) { + VL_ASSIGNBIT_IO(m_bits - 1, result.m_lo.back()); + VL_ASSIGNBIT_II(m_bits - 1, result.m_hi.back(), 0); + } + bool fixed = false; + bool contiguous = true; + for (uint32_t bit = 0; bit < m_bits; ++bit) { + if (VL_BITISSET_W(result.m_mask, bit)) { + fixed = true; + } else if (fixed) { + contiguous = false; + } + } + if (contiguous) result.m_mask.clear(); + return clip(result, read(lop), read(hip)); + } + Value lastValue(const Range& range) const { + Range reversed = range; + Value lower = range.m_hi; + for (uint32_t word = 0; word < m_words; ++word) { + reversed.m_lo[word] = ~reversed.m_lo[word]; + lower[word] = ~lower[word]; + } + reversed.m_lo.back() &= VL_MASK_E(m_bits); + lower.back() &= VL_MASK_E(m_bits); + Value result; + const bool found VL_ATTR_UNUSED = patternAtLeast(reversed, lower, result); + assert(found); + for (EData& word : result) word = ~word; + result.back() &= VL_MASK_E(m_bits); + return result; + } + Search hasValue(uint32_t bin, const Range& range) const; + Search intersects(uint32_t bin, const Range& filter) const; +}; + +// One bounded search. Shared ordered decisions avoid expanding the complement of wildcard +// exclusions; the graph is discarded with the query, so instances retain none of it. +class VlCoverpoint::ValueData::Query final { + struct Decision final { + uint32_t m_position; // One-based value-bit position; zero denotes a terminal + uint32_t m_low; // Child node ID for a zero-valued ordering bit + uint32_t m_high; // Child node ID for a one-valued ordering bit + uint32_t m_inverse; // Complement node ID, or UINT32_MAX if not cached + }; + + const ValueData& m_data; // Value width and ordering of the queried coverpoint + std::vector m_decisions{{0, 0, 0, 1}, {0, 1, 1, 0}}; // Nodes; 0=false, 1=true + // (Position, low, high) -> canonical node ID + std::map, uint32_t> m_unique; + std::map, uint32_t> m_combined; // Cached intersection roots + uint32_t m_work = 0; // Graph steps consumed by this query + bool m_limited = false; // The work limit was exceeded, so the result is unknown + + bool step() { + if (m_limited) return false; + if (++m_work <= QUERY_WORK_LIMIT) return true; + m_limited = true; + return false; + } + uint32_t decision(uint32_t position, uint32_t low, uint32_t high) { + if (m_limited) return 0; + if (low == high) return low; + const auto key = std::make_tuple(position, low, high); + const auto it = m_unique.find(key); + if (it != m_unique.end()) return it->second; + const uint32_t result = static_cast(m_decisions.size()); + m_decisions.push_back({position, low, high, UINT32_MAX}); + m_unique.emplace(key, result); + return result; + } + uint32_t rangeDecision(const Range& range, uint32_t position, uint32_t bounds, + std::vector>& cache) { + if (!step()) return 0; + uint32_t& cached = cache[position][bounds]; + if (cached != UINT32_MAX) return cached; + const uint32_t bit = position - 1; + const uint32_t lower = m_data.orderBit(range.m_lo, bit); + const uint32_t upper = m_data.orderBit(range.m_hi, bit); + uint32_t children[2] = {0, 0}; + for (uint32_t value = 0; value < 2; ++value) { + if ((!range.m_mask.empty() && VL_BITISSET_W(range.m_mask, bit) && value != lower) + || ((bounds & 1U) && value < lower) || ((bounds & 2U) && value > upper)) { + continue; + } + const uint32_t next = ((bounds & 1U) && value == lower ? 1U : 0U) + | ((bounds & 2U) && value == upper ? 2U : 0U); + children[value] = rangeDecision(range, position - 1, next, cache); + } + cached = decision(position, children[0], children[1]); + return cached; + } + +public: + explicit Query(const ValueData& data) + : m_data{data} {} + bool limited() const { return m_limited; } + uint32_t intersect(uint32_t lhs, uint32_t rhs) { + if (!step()) return 0; + if (lhs == rhs) return lhs; + if (!lhs || !rhs) return 0; + if (lhs == 1) return rhs; + if (rhs == 1) return lhs; + if (rhs < lhs) std::swap(lhs, rhs); + const auto key = std::make_pair(lhs, rhs); + const auto it = m_combined.find(key); + if (it != m_combined.end()) return it->second; + // Recursive calls can grow decisions, so do not retain references into it. + const Decision left = m_decisions[lhs]; + const Decision right = m_decisions[rhs]; + const uint32_t position = std::max(left.m_position, right.m_position); + const uint32_t low = intersect(left.m_position == position ? left.m_low : lhs, + right.m_position == position ? right.m_low : rhs); + const uint32_t high = intersect(left.m_position == position ? left.m_high : lhs, + right.m_position == position ? right.m_high : rhs); + const uint32_t result = decision(position, low, high); + m_combined.emplace(key, result); + return result; + } + uint32_t negate(uint32_t root) { + if (!step()) return 0; + if (m_decisions[root].m_inverse != UINT32_MAX) return m_decisions[root].m_inverse; + const Decision node = m_decisions[root]; + const uint32_t low = negate(node.m_low); + const uint32_t high = negate(node.m_high); + const uint32_t result = decision(node.m_position, low, high); + m_decisions[root].m_inverse = result; + m_decisions[result].m_inverse = root; + return result; + } + uint32_t rangeRoot(const Range& range) { + if (m_data.less(range.m_hi, range.m_lo)) return 0; + std::vector> cache(m_data.m_bits + 1); + for (auto& entry : cache) entry.fill(UINT32_MAX); + cache[0].fill(1); + return rangeDecision(range, m_data.m_bits, 3, cache); + } +}; + +VlCoverpoint::ValueData::Search VlCoverpoint::ValueData::hasValue(uint32_t bin, + const Range& range) const { + if (m_values[bin].m_transition || m_exclusions.empty() || !excluded(view(range.m_lo))) { + return Search::VALUE; + } + const Value last = lastValue(range); + if (!excluded(view(last))) return Search::VALUE; + if (last == range.m_lo) return Search::EMPTY; + // Try cheap witnesses first; only difficult queries need a bounded symbolic search. + if (m_bits > QUERY_DEPTH_LIMIT) return Search::DEPTH_LIMIT; + Query query{*this}; + uint32_t root = query.rangeRoot(range); + for (const Range& exclusion : m_exclusions) { + root = query.intersect(root, query.negate(query.rangeRoot(exclusion))); + if (!root) break; + } + if (query.limited()) return Search::WORK_LIMIT; + return root ? Search::VALUE : Search::EMPTY; +} + +VlCoverpoint::ValueData::Search VlCoverpoint::ValueData::intersects(uint32_t bin, + const Range& filter) const { + Search result = Search::EMPTY; + for (const Range& source : m_values[bin].m_ranges) { + Range range = source; + if (!clip(range, filter.m_lo, filter.m_hi)) continue; + const Search search = hasValue(bin, range); + if (search == Search::VALUE) return search; + if (search != Search::EMPTY) result = search; + } + return result; +} + +VlCoverpoint::VlCoverpoint() = default; +VlCoverpoint::~VlCoverpoint() = default; + +void VlCoverpoint::valueType(uint32_t bits, bool isSigned) { + assert(!m_valuesp); + m_valuesp.reset(new ValueData{bits, isSigned, m_total}); +} + +void VlCoverpoint::valueRanges(std::initializer_list entries) { + ValueData& data = *m_valuesp; + assert(!data.m_frozen); + const uint32_t words = data.m_words; + assert(entries.size() % (1 + 2 * words) == 0); + for (const EData* entryp = entries.begin(); entryp != entries.end(); entryp += 1 + 2 * words) { + data.m_values[entryp[0]].m_ranges.push_back( + {data.read(WDataInP::external(entryp + 1)), + data.read(WDataInP::external(entryp + 1 + words)), + {}}); + } +} + +void VlCoverpoint::valuePatterns(std::initializer_list entries) { + ValueData& data = *m_valuesp; + assert(!data.m_frozen); + const uint32_t words = data.m_words; + assert(entries.size() % (1 + 4 * words) == 0); + for (const EData* entryp = entries.begin(); entryp != entries.end(); entryp += 1 + 4 * words) { + ValueData::Range range; + if (data.pattern(WDataInP::external(entryp + 1), WDataInP::external(entryp + 1 + words), + WDataInP::external(entryp + 1 + 2 * words), + WDataInP::external(entryp + 1 + 3 * words), range)) { + data.m_values[entryp[0]].m_ranges.push_back(std::move(range)); + } + } +} + +void VlCoverpoint::valueTransitions(std::initializer_list bins) { + for (const uint32_t bin : bins) m_valuesp->m_values[bin].m_transition = true; +} + +bool VlCoverpoint::liveBin(uint32_t bin) const { + const ValueData& data = *m_valuesp; + ValueData::Search limit = ValueData::Search::EMPTY; + for (const ValueData::Range& range : data.m_values[bin].m_ranges) { + const ValueData::Search search = data.hasValue(bin, range); + if (search == ValueData::Search::VALUE) return true; + if (search != ValueData::Search::EMPTY) limit = search; + } + if (limit == ValueData::Search::EMPTY) return false; + // Keep a bin whose exclusions cannot be analyzed, rather than stop the simulation. + const VlCovNamer& namer = namerFor(bin); + VL_WARN_MT( + namer.file(), namer.line(), "", + limit == ValueData::Search::WORK_LIMIT + ? "Coverage bin exclusions exceed the decision-graph work limit; bin retained" + : "Coverage bin exclusions exceed the decision-graph depth limit; bin retained"); + return true; +} + +void VlCoverpoint::valueFinalize() { + ValueData& data = *m_valuesp; + assert(!data.m_frozen); + for (const VlCovNamer& namer : m_namers) { + const bool exclusion = namer.set() == VlCovBinKind::KIND_IGNORE + || namer.set() == VlCovBinKind::KIND_ILLEGAL; + for (uint32_t bin = namer.base(); bin < namer.base() + namer.count(); ++bin) { + ValueData::Values& values = data.m_values[bin]; + data.normalize(values.m_ranges); + if (exclusion && !values.m_transition) { + data.m_exclusions.insert(data.m_exclusions.end(), values.m_ranges.begin(), + values.m_ranges.end()); + } + } + } + data.m_regularExclusions = data.normalize(data.m_exclusions); + m_crossToBin.clear(); + std::fill(m_crossIdx.begin(), m_crossIdx.end(), -1); + m_normal = 0; + // Normal bins without values leave the report and the coverage denominator. + for (const VlCovNamer& namer : m_namers) { + const bool normal = namer.set() == VlCovBinKind::KIND_NORMAL; + for (uint32_t bin = namer.base(); bin < namer.base() + namer.count(); ++bin) { + if (normal && !liveBin(bin)) continue; + data.m_reported.push_back(bin); + if (!normal) continue; + m_crossIdx[bin] = static_cast(m_normal++); + m_crossToBin.push_back(bin); + } + } + data.m_frozen = true; +} + +void VlCoverpoint::valueRelease() { + ValueData& data = *m_valuesp; + assert(data.m_frozen); + std::vector{}.swap(data.m_values); +} + +bool VlCoverpoint::valueExcluded(QData value) const { + VlWide words; + VL_SET_WQ(words, value); + return valueExcludedW(words); +} + +bool VlCoverpoint::valueExcludedW(WDataInP valuep) const { return m_valuesp->excluded(valuep); } + void VlCoverpoint::init(const char* hier, uint32_t atLeast, uint32_t nBins) { m_hier = hier; m_atLeast = atLeast; @@ -63,7 +551,7 @@ void VlCoverpoint::addNamer(VlCovBinKind set, uint32_t count, VlCovBinNaming nam std::string VlCoverpoint::normalBinName(uint32_t crossIdx) const { // Build the bin name based on the bin index - return binName(m_crossToBin[crossIdx]); + return declaredBinName(m_crossToBin[crossIdx]); } const VlCovNamer& VlCoverpoint::namerFor(uint32_t i) const { @@ -75,19 +563,30 @@ const VlCovNamer& VlCoverpoint::namerFor(uint32_t i) const { return *std::prev(it); } -std::string VlCoverpoint::binName(uint32_t i) const { - const VlCovNamer& nm = namerFor(i); +std::string VlCoverpoint::declaredBinName(uint32_t bin) const { + const VlCovNamer& nm = namerFor(bin); std::string name = nm.name(); - if (nm.naming() == VlCovBinNaming::Array) name += '[' + std::to_string(i - nm.base()) + ']'; + if (nm.naming() == VlCovBinNaming::Array) name += '[' + std::to_string(bin - nm.base()) + ']'; return name; } +uint32_t VlCoverpoint::reportedBin(uint32_t i) const { + return m_valuesp ? m_valuesp->m_reported[i] : i; +} + +uint32_t VlCoverpoint::binCount() const { + return m_valuesp ? static_cast(m_valuesp->m_reported.size()) : m_total; +} + +std::string VlCoverpoint::binName(uint32_t i) const { return declaredBinName(reportedBin(i)); } + #if VM_COVERAGE void VlCoverpoint::registerBins(VerilatedCovContext* covcontextp, const char* page) { - for (uint32_t i = 0; i < binCount(); ++i) { + for (uint32_t reported = 0; reported < binCount(); ++reported) { + const uint32_t i = reportedBin(reported); const VlCovNamer& nm = namerFor(i); - const VlCovBinKind kind = binKind(i); - const std::string binp = binName(i); + const VlCovBinKind kind = binKind(reported); + const std::string binp = binName(reported); const std::string full = m_hier + "." + binp; const std::string lineStr = std::to_string(nm.line()); const std::string colStr = std::to_string(nm.col()); @@ -137,12 +636,18 @@ void VlCoverCross::init(const char* hier, uint32_t dims, VlCoverpoint* const* cp void VlCoverCross::addBin(VlCovBinKind kind, std::initializer_list selection, const char* namep, const char* filep, int line, int col) { if (!m_numAutoBins) return; // An empty product creates no cross bin. + addBinImpl(kind, selection.begin(), static_cast(selection.size()), namep, filep, + line, col, m_explicitp->numBins); +} + +void VlCoverCross::addBinImpl(VlCovBinKind kind, const uint64_t* sourcep, uint32_t words, + const char* namep, const char* filep, int line, int col, + uint32_t iffIndex) { Explicit& data = *m_explicitp; - const uint32_t words = m_numAutoBins / 64 + (m_numAutoBins % 64 != 0); - assert(selection.size() == words); + assert(words == VL_BITWORD_Q(static_cast(m_numAutoBins) + VL_QUADSIZE - 1)); assert(data.numBins < data.bins.size()); uint64_t* const selectionp = data.selectionp + static_cast(data.numBins) * words; - std::copy(selection.begin(), selection.end(), selectionp); + std::copy(sourcep, sourcep + words, selectionp); Bin& bin = data.bins[data.numBins++]; bin.selectionp = selectionp; bin.namep = namep; @@ -150,9 +655,11 @@ void VlCoverCross::addBin(VlCovBinKind kind, std::initializer_list sel bin.line = line; bin.col = col; bin.kind = kind; + bin.iffIndex = iffIndex; if (kind == VlCovBinKind::KIND_NORMAL) ++data.normalBins; - uint32_t word = 0; - for (const uint64_t bits : selection) { data.wordsp[word++].autoExcluded |= bits; } + for (uint32_t word = 0; word < words; ++word) { + data.wordsp[word].autoExcluded |= selectionp[word]; + } } void VlCoverCross::finalizeBins() { @@ -226,7 +733,7 @@ void VlCoverCross::sampleSingleTuple(uint32_t idx, const bool* binIffs) { return; } for (Bin& bin : data.bins) { - if (T_ApplyIffs && !*binIffs++) continue; + if (T_ApplyIffs && !binIffs[bin.iffIndex]) continue; if (bin.selectionp[word] & bit) incrementBin(bin); } } @@ -247,8 +754,8 @@ void VlCoverCross::sampleBins(const bool* binIffs) { cached[i] = {word, wordsp[word].hitBits}; } for (uint64_t binIdx = 0; binIdx < bins; ++binIdx) { - if (T_ApplyIffs && !*binIffs++) continue; Bin& bin = data.bins[binIdx]; + if (T_ApplyIffs && !binIffs[bin.iffIndex]) continue; bool matched = false; if (T_Touched == 1) { matched = (bin.selectionp[cached[0].index] & cached[0].bits) != 0; @@ -289,6 +796,7 @@ void VlCoverCross::sampleHitWords(const bool* binIffs) { } void VlCoverCross::sample(const bool* binIffs) { + if (VL_UNLIKELY(!m_numAutoBins)) return; // Fast path: if any dimension had no Normal-bin hit, the cross cannot hit. bool single = true; for (uint32_t d = 0; d < m_dims; ++d) { @@ -313,9 +821,9 @@ void VlCoverCross::sample(const bool* binIffs) { return; } bool enabled = true; - if (hasExplicitBins() && binIffs && !binIffs[0]) { - const bool* const endp = binIffs + m_explicitp->bins.size(); - enabled = std::find(binIffs + 1, endp, true) != endp; + if (hasExplicitBins() && binIffs && !binIffs[m_explicitp->bins[0].iffIndex]) { + enabled = std::any_of(m_explicitp->bins.begin() + 1, m_explicitp->bins.end(), + [binIffs](const Bin& bin) { return binIffs[bin.iffIndex]; }); if (!enabled && m_explicitp->autoBins.empty()) return; } for (uint32_t d = 0; d < m_dims; ++d) { @@ -412,6 +920,247 @@ void VlCoverCross::registerBins(VerilatedCovContext* covcontextp, const char* pa } #endif // VM_COVERAGE +//============================================================================= +// VlCoverCrossDyn + +class VlCoverCrossDyn::Layout final { + friend class VlCoverCrossDyn; + + using Mask = std::vector; + using Search = VlCoverpoint::ValueData::Search; + struct Selected final { + Bin m_info{}; // Declaration metadata, bin kind, and original iff index + Mask m_mask; // Tuple-selection bitmap for the declared bin + }; + uint32_t m_tuples = 0; // Cartesian product of live coverpoint-bin counts + uint32_t m_words = 0; // 64-bit words per tuple-selection bitmap + std::vector m_dimensions; // Coverpoint bindings, hit lists, and tuple strides + std::vector m_counts; // Dense automatic-bin counters, one slot per flat tuple ID + std::vector m_bins; // Final compacted explicit-bin records + std::vector m_hitWords; // Auto-exclusion and hit masks, plus touched-word IDs + std::vector m_autoBins; // Flat tuple IDs retained as automatic cross bins + std::vector m_binWords; // Packed nonzero selection-word indices per explicit bin + std::vector m_selections; // Contiguous bitmaps for finalized explicit bins + Explicit m_explicitData{{nullptr, 0}, + nullptr, + {nullptr, 0}, + {nullptr, 0}, + nullptr}; // Storage views bound to the base sampler + std::vector m_stack; // Postfix evaluation stack for construction-time selections + std::vector m_selected; // Declared bins pending exclusion and compaction + uint32_t m_selectDimension = 0; // Dimension whose binsof selection is being built + uint32_t m_selectFirst = 0; // First declared coverpoint bin named by the binsof term + uint32_t m_selectEnd = 0; // One past the last declared coverpoint bin named by binsof + bool m_negate = false; // Complement the completed dimension membership mask + Search m_limit = Search::EMPTY; // A search limit left the current bin's selection unknown + std::vector m_allowed; // Normal-bin membership mask for the current dimension + + static void setRange(Mask& mask, uint64_t first, uint64_t end) { + while (first < end) { + const uint64_t bit = VL_BITBIT_Q(first); + const uint64_t bits = std::min(VL_QUADSIZE - bit, end - first); + mask[VL_BITWORD_Q(first)] + |= (bits == VL_QUADSIZE ? ~uint64_t{0} : (uint64_t{1} << bits) - 1) << bit; + first += bits; + } + } + bool named(uint32_t index) const { + const uint32_t bin = m_dimensions[m_selectDimension].cpp->m_crossToBin[index]; + return bin >= m_selectFirst && bin < m_selectEnd; + } + void range(WDataInP lop, WDataInP hip) { + const VlCoverpoint* const cpp = m_dimensions[m_selectDimension].cpp; + const VlCoverpoint::ValueData& data = *cpp->m_valuesp; + const VlCoverpoint::ValueData::Range filter{data.read(lop), data.read(hip), {}}; + for (uint32_t i = 0; i < m_allowed.size(); ++i) { + if (m_allowed[i] || !named(i)) continue; + const Search search = data.intersects(cpp->m_crossToBin[i], filter); + if (search == Search::VALUE) { + m_allowed[i] = true; + } else if (search != Search::EMPTY) { + m_limit = search; + } + } + } +}; + +VlCoverCrossDyn::VlCoverCrossDyn() + : VlCoverCross{0, 0} + , m_layoutp{new Layout} {} + +VlCoverCrossDyn::~VlCoverCrossDyn() = default; + +void VlCoverCrossDyn::init(const char* hier, uint32_t dims, VlCoverpoint* const* cps, + const char* file, int line, int col) { + Layout& data = *m_layoutp; + uint64_t tuples = std::any_of(cps, cps + dims, + [](const VlCoverpoint* cpp) { return !cpp->normalBinCount(); }) + ? 0 + : 1; + for (uint32_t i = 0; i < dims; ++i) tuples *= cps[i]->normalBinCount(); + // Verilation bounds the product of the declared bins, which live bins cannot exceed. + assert(tuples <= UINT32_MAX); + data.m_tuples = static_cast(tuples); + data.m_words = VL_BITWORD_Q(static_cast(data.m_tuples) + VL_QUADSIZE - 1); + data.m_dimensions.resize(dims); + data.m_counts.resize(data.m_tuples, 0); + shape(dims, data.m_tuples); + bindStorage(data.m_dimensions.data(), data.m_counts.data()); + VlCoverCross::init(hier, dims, cps, file, line, col); +} + +void VlCoverCrossDyn::selectAll() { + Layout& data = *m_layoutp; + data.m_stack.emplace_back(data.m_words, ~uint64_t{0}); + if (data.m_words) data.m_stack.back().back() &= VL_MASK_Q(data.m_tuples); +} + +void VlCoverCrossDyn::selectDim(uint32_t dim, uint32_t first, uint32_t end, bool negated, + bool intersect) { + Layout& data = *m_layoutp; + data.m_selectDimension = dim; + data.m_selectFirst = first; + data.m_selectEnd = end; + data.m_negate = negated; + data.m_allowed.assign(data.m_dimensions[dim].bins, false); + if (!intersect) { + for (uint32_t i = 0; i < data.m_allowed.size(); ++i) data.m_allowed[i] = data.named(i); + } +} + +void VlCoverCrossDyn::selectRange(QData lo, QData hi) { + VlWide low; + VlWide high; + VL_SET_WQ(low, lo); + VL_SET_WQ(high, hi); + m_layoutp->range(low, high); +} + +void VlCoverCrossDyn::selectRangeW(WDataInP lop, WDataInP hip) { m_layoutp->range(lop, hip); } + +void VlCoverCrossDyn::selectDimEnd() { + Layout& data = *m_layoutp; + data.m_stack.emplace_back(data.m_words, 0); + Layout::Mask& mask = data.m_stack.back(); + const Dimension& dim = data.m_dimensions[data.m_selectDimension]; + // Each run of adjacent selected bins covers one contiguous tuple span per period. + std::vector> runs; // [first, end) bin indices + for (uint32_t i = 0; i < dim.bins;) { + if (data.m_allowed[i] == data.m_negate) { + ++i; + continue; + } + const uint32_t begin = i++; + while (i < dim.bins && data.m_allowed[i] != data.m_negate) ++i; + runs.emplace_back(begin, i); + } + const uint64_t stride = dim.stride; + const uint64_t period = stride * dim.bins; + for (uint64_t base = 0; base < data.m_tuples; base += period) { + for (const auto& run : runs) { + Layout::setRange(mask, base + run.first * stride, base + run.second * stride); + } + } +} + +void VlCoverCrossDyn::selectAnd() { + Layout& data = *m_layoutp; + Layout::Mask rhs = std::move(data.m_stack.back()); + data.m_stack.pop_back(); + for (uint32_t word = 0; word < data.m_words; ++word) data.m_stack.back()[word] &= rhs[word]; +} + +void VlCoverCrossDyn::selectOr() { + Layout& data = *m_layoutp; + Layout::Mask rhs = std::move(data.m_stack.back()); + data.m_stack.pop_back(); + for (uint32_t word = 0; word < data.m_words; ++word) data.m_stack.back()[word] |= rhs[word]; +} + +void VlCoverCrossDyn::selectBin(VlCovBinKind kind, const char* namep, const char* filep, int line, + int col, uint32_t iffIndex) { + Layout& data = *m_layoutp; + if (VL_UNLIKELY(data.m_limit != Layout::Search::EMPTY)) { + // Ignore a bin whose selection cannot be analyzed, rather than stop the simulation. + VL_WARN_MT( + filep, line, "", + data.m_limit == Layout::Search::WORK_LIMIT + ? "Cross bin selection exceeds the decision-graph work limit; bin ignored" + : "Cross bin selection exceeds the decision-graph depth limit; bin ignored"); + data.m_limit = Layout::Search::EMPTY; + data.m_stack.pop_back(); + return; + } + Bin bin{}; + bin.kind = kind; + bin.namep = namep; + bin.filep = filep; + bin.line = line; + bin.col = col; + bin.iffIndex = iffIndex; + data.m_selected.push_back({bin, std::move(data.m_stack.back())}); + data.m_stack.pop_back(); +} + +void VlCoverCrossDyn::finalizeBins() { + Layout& data = *m_layoutp; + Layout::Mask excluded(data.m_words, 0); + for (const Layout::Selected& bin : data.m_selected) { + if (bin.m_info.kind == VlCovBinKind::KIND_NORMAL) continue; + for (uint32_t word = 0; word < data.m_words; ++word) excluded[word] |= bin.m_mask[word]; + } + for (Layout::Selected& bin : data.m_selected) { + if (bin.m_info.kind != VlCovBinKind::KIND_NORMAL) continue; + for (uint32_t word = 0; word < data.m_words; ++word) bin.m_mask[word] &= ~excluded[word]; + } + data.m_selected.erase(std::remove_if(data.m_selected.begin(), data.m_selected.end(), + [](const Layout::Selected& bin) { + return std::all_of( + bin.m_mask.begin(), bin.m_mask.end(), + [](uint64_t word) { return !word; }); + }), + data.m_selected.end()); + if (data.m_selected.empty()) return; + Layout::Mask occupied(data.m_words, 0); + uint64_t binWords = 0; + for (const Layout::Selected& bin : data.m_selected) { + for (uint32_t word = 0; word < data.m_words; ++word) { + occupied[word] |= bin.m_mask[word]; + if (bin.m_mask[word]) ++binWords; + } + } + // Selection masks have no bits past m_tuples, so this counts the automatic bins. + uint32_t autoBins = data.m_tuples; + for (const uint64_t word : occupied) autoBins -= VL_COUNTONES_Q(word); + data.m_autoBins.resize(autoBins); + data.m_bins.resize(data.m_selected.size()); + data.m_hitWords.resize(data.m_words); + data.m_binWords.resize(binWords); + data.m_selections.resize(data.m_selected.size() * data.m_words); + data.m_explicitData = {{data.m_bins.data(), data.m_bins.size()}, + data.m_hitWords.data(), + {data.m_autoBins.data(), data.m_autoBins.size()}, + {data.m_binWords.data(), data.m_binWords.size()}, + data.m_selections.data()}; + bindStorage(data.m_dimensions.data(), data.m_counts.data(), &data.m_explicitData); + for (const Layout::Selected& bin : data.m_selected) { + addBinImpl(bin.m_info.kind, bin.m_mask.data(), data.m_words, bin.m_info.namep, + bin.m_info.filep, bin.m_info.line, bin.m_info.col, bin.m_info.iffIndex); + } + VlCoverCross::finalizeBins(); + data.m_selected.clear(); + data.m_stack.clear(); +} + +//============================================================================= +// VlCovergroupInst + +VlCoverCrossDyn* VlCovergroupInst::addCrossDyn() { + VlCoverCrossDyn* const cxp = new VlCoverCrossDyn{}; + m_items.emplace_back(cxp); + return cxp; +} + //============================================================================= // VlCovergroupType / VlCovRegistry diff --git a/include/verilated_covergroup.h b/include/verilated_covergroup.h index c82328172..92cdf8541 100644 --- a/include/verilated_covergroup.h +++ b/include/verilated_covergroup.h @@ -99,6 +99,10 @@ public: // VlCoverpointT adds the inline hit-list array and the incrementBin write // path; the cross holds VlCoverpoint* and reads via hitCount()/hitList(). class VlCoverpoint VL_NOT_FINAL : public VlCoverpointIf { + struct ValueData; + std::unique_ptr m_valuesp; // Optional value metadata and exclusion state + friend class VlCoverCrossDyn; + protected: // MEMBERS (protected so VlCoverpointT::incrementBin can update them) std::string m_hier; // "covergroup.coverpoint" @@ -120,10 +124,15 @@ private: const VlCovNamer& namerFor(uint32_t i) const; // obtain the bin-specific name producer void addNamer(VlCovBinKind set, uint32_t count, VlCovBinNaming naming, const char* name, const char* file, int line, int col); + // Declared bin index of the i-th bin reported through VlCoverpointIf + uint32_t reportedBin(uint32_t i) const; + std::string declaredBinName(uint32_t bin) const; // Name of a declared bin index + bool liveBin(uint32_t bin) const; // Normal bin keeps a value outside the exclusions public: // CONSTRUCTORS - VlCoverpoint() = default; + VlCoverpoint(); + ~VlCoverpoint() override; // METHODS // ---- configuration (from generated constructor) ---- @@ -137,6 +146,22 @@ public: } void registerBins(VerilatedCovContext* covcontextp, const char* page); + /// Configure construction-time value metadata for exclusions and cross selections. + void valueType(uint32_t bits, bool isSigned); + /// Describe bin values as {bin, low words, high words} entries, without enumerating them. + void valueRanges(std::initializer_list entries); + /// Describe wildcard patterns as {bin, value words, mask words, low words, high words}. + void valuePatterns(std::initializer_list entries); + /// State exclusions do not remove values from these transition bins. + void valueTransitions(std::initializer_list bins); + /// Apply exclusions and freeze the live Normal-bin index space used by crosses. + void valueFinalize(); + /// Drop the per-bin values once every cross has been built; sampling needs only exclusions. + void valueRelease(); + /// Test state exclusions independently of sampling-time iff guards. + bool valueExcluded(QData value) const; + bool valueExcludedW(WDataInP valuep) const; + // ---- hot path (from generated sample()) ---- // Clear the hit list at the start of each sample() for a cross-fed coverpoint. void clearHitList() { m_hitCount = 0; } @@ -152,19 +177,17 @@ public: std::string normalBinName(uint32_t crossIdx) const; // name of the crossIdx-th Normal bin // ---- VlCoverpointIf ---- - uint32_t binCount() const override { return m_total; } + /// Bins removed for having no value (IEEE 1800-2023 19.11.1) are not reported. + uint32_t binCount() const override; std::string binName(uint32_t i) const override; // Deliberately not on VlCoverpointIf: only coverage-database registration needs it. - VlCovBinKind binKind(uint32_t i) const { return namerFor(i).set(); } + VlCovBinKind binKind(uint32_t i) const { return namerFor(reportedBin(i)).set(); } void coverageParts(double& covered, double& total) const override { // Count Normal bins that reached option.at_least on demand, so the hot // path (incrementBin) stays a plain counter bump. uint32_t numCovered = 0; - for (const VlCovNamer& nm : m_namers) { - if (nm.set() != VlCovBinKind::KIND_NORMAL) continue; - for (uint32_t i = nm.base(); i < nm.base() + nm.count(); ++i) { - if (m_counts[i] >= m_atLeast) ++numCovered; - } + for (const uint32_t bin : m_crossToBin) { + if (m_counts[bin] >= m_atLeast) ++numCovered; } covered = numCovered; total = m_normal; @@ -211,7 +234,7 @@ public: /// built on demand for automatic bins; explicit bins select sets of tuples /// and replace the corresponding automatic cross bins. Explicit selections /// are intersected with hit-tuple words once per sample. -/// VlCoverCrossT owns the fixed arrays. This shared core does not allocate bin +/// VlCoverCrossT and VlCoverCrossDyn own their storage. This shared core does not allocate bin /// storage, and its borrowed storage pointers remain valid for the instance. class VlCoverCross VL_NOT_FINAL : public VlCoverpointIf { @@ -223,15 +246,16 @@ protected: uint32_t stride; // Flat-index stride }; struct Bin final { - const uint64_t* selectionp; // Slice of the fixed selection storage + const uint64_t* selectionp; // Slice of the cross's selection storage const char* namep; // Explicit bin name const char* filep; // Bin declaration file + const uint32_t* wordIndicesp = nullptr; // Slice of the packed selection-word indices int line; // Bin declaration line int col; // Bin declaration column VlCovBinKind kind = VlCovBinKind::KIND_NORMAL; // Normal, ignore, or illegal bin uint32_t count = 0; // Samples matching the selection and guard uint32_t numWords = 0; // Number of nonzero selection-word indices - const uint32_t* wordIndicesp = nullptr; // Slice of the packed selection-word indices + uint32_t iffIndex = 0; // Original guard index, including bins removed during finalization }; struct Word final { uint64_t autoExcluded = 0; // Tuples replaced by explicit bins @@ -278,7 +302,7 @@ private: // storable anyway: m_flatCountsp alone would need 16GB. uint32_t m_numAutoBins = 0; // Product of per-dim Normal bin counts uint32_t m_numCovered = 0; // Distinct bins hit >= 1 (maintained incrementally) - Dimension* m_dimensionsp = nullptr; // [m_dims], owned by VlCoverCrossT + Dimension* m_dimensionsp = nullptr; // [m_dims], owned by the concrete cross runtime uint32_t* m_flatCountsp = nullptr; // [m_numAutoBins] Per-bin hit counts Explicit* m_explicitp = nullptr; // Absent for automatic-only crosses @@ -326,19 +350,25 @@ protected: m_flatCountsp = countsp; m_explicitp = explicitp; } + void shape(uint32_t dims, uint32_t tuples) { + m_dims = dims; + m_numAutoBins = tuples; + } + void addBinImpl(VlCovBinKind kind, const uint64_t* selectionp, uint32_t words, + const char* namep, const char* filep, int line, int col, uint32_t iffIndex); public: VL_UNCOPYABLE(VlCoverCross); // METHODS // ---- configuration (from generated constructor, after coverpoints init'd) ---- - void init(const char* hier, uint32_t dims, VlCoverpoint* const* cps, const char* file, - int line, int col); + virtual void init(const char* hier, uint32_t dims, VlCoverpoint* const* cps, const char* file, + int line, int col); /// Add a cross bin using a verilation-time bitmap of selected Normal-bin tuples. void addBin(VlCovBinKind kind, std::initializer_list selection, const char* namep, const char* filep, int line, int col); /// Retain only automatic cross bins not selected by any explicit bin. - void finalizeBins(); + virtual void finalizeBins(); void registerBins(VerilatedCovContext* covcontextp, const char* page); // ---- hot path (from generated sample(), after all coverpoints sampled) ---- @@ -406,6 +436,36 @@ public: } }; +// Construction-time cross layout over finalized coverpoints, sharing the sampling core. +class VlCoverCrossDyn final : public VlCoverCross { + class Layout; + std::unique_ptr m_layoutp; // Owned cross storage and construction-time selections + +public: + // CONSTRUCTORS + VlCoverCrossDyn(); + ~VlCoverCrossDyn() override; + + // METHODS + /// Initialize after all feeding coverpoints have finalized their live bins. + void init(const char* hier, uint32_t dims, VlCoverpoint* const* cps, const char* file, + int line, int col) override; + /// Build cross-bin selections in postfix order. + void selectAll(); + /// Start a binsof term over the live bins declared in [first, end) of dimension 'dim'. + void selectDim(uint32_t dim, uint32_t first, uint32_t end, bool negated, bool intersect); + void selectRange(QData lo, QData hi); + void selectRangeW(WDataInP lop, WDataInP hip); + void selectDimEnd(); + void selectAnd(); + void selectOr(); + // Save a selection without renumbering guards when empty bins are removed. + void selectBin(VlCovBinKind kind, const char* namep, const char* filep, int line, int col, + uint32_t iffIndex); + /// Apply cross exclusions and bind finalized storage to the sampling core. + void finalizeBins() override; +}; + class VlCovergroupType; //============================================================================= @@ -456,6 +516,7 @@ public: m_items.emplace_back(cxp); return cxp; // borrowed by the generated class } + VlCoverCrossDyn* addCrossDyn(); // ---- attach counting (from VlCovInstHandle) ---- void attachInc() { ++m_attachCount; } diff --git a/src/V3AstAttr.h b/src/V3AstAttr.h index 6623605b8..8a479dd6f 100644 --- a/src/V3AstAttr.h +++ b/src/V3AstAttr.h @@ -874,6 +874,7 @@ inline std::ostream& operator<<(std::ostream& os, const VBranchPred& rhs) { macro(COVERGROUP_ADD_BIN, "addBin", false, "r+") \ macro(COVERGROUP_ADD_COVERPOINT, "addCoverpoint", false, "") \ macro(COVERGROUP_ADD_CROSS, "addCross", false, "") \ + macro(COVERGROUP_ADD_CROSS_DYN, "addCrossDyn", false, "") \ macro(COVERGROUP_ADD_SINGLE_NAMER, "addSingleNamer", false, "r+") \ macro(COVERGROUP_ATTACH, "attach", false, "r") \ macro(COVERGROUP_CLEAR_HIT_LIST, "clearHitList", false, "") \ @@ -886,6 +887,22 @@ inline std::ostream& operator<<(std::ostream& os, const VBranchPred& rhs) { macro(COVERGROUP_REGISTER_BINS, "registerBins", false, "rr") \ macro(COVERGROUP_SAMPLE, "sample", false, "") \ macro(COVERGROUP_SAMPLE_IFFS, "sample", false, "r") \ + macro(COVERGROUP_SELECT_ALL, "selectAll", false, "") \ + macro(COVERGROUP_SELECT_AND, "selectAnd", false, "") \ + macro(COVERGROUP_SELECT_BIN, "selectBin", false, "r+") \ + macro(COVERGROUP_SELECT_DIM, "selectDim", false, "r+") \ + macro(COVERGROUP_SELECT_DIM_END, "selectDimEnd", false, "") \ + macro(COVERGROUP_SELECT_OR, "selectOr", false, "") \ + macro(COVERGROUP_SELECT_RANGE, "selectRange", false, "rr") \ + macro(COVERGROUP_SELECT_RANGE_W, "selectRangeW", false, "rr") \ + macro(COVERGROUP_VALUE_EXCLUDED, "valueExcluded", PURE, "r") \ + macro(COVERGROUP_VALUE_EXCLUDED_W, "valueExcludedW", PURE, "r") \ + macro(COVERGROUP_VALUE_FINALIZE, "valueFinalize", false, "") \ + macro(COVERGROUP_VALUE_PATTERNS, "valuePatterns", false, "r") \ + macro(COVERGROUP_VALUE_RANGES, "valueRanges", false, "r") \ + macro(COVERGROUP_VALUE_RELEASE, "valueRelease", false, "") \ + macro(COVERGROUP_VALUE_TRANSITIONS, "valueTransitions", false, "r") \ + macro(COVERGROUP_VALUE_TYPE, "valueType", false, "rr") \ macro(DYN_AT_WRITE_APPEND, "atWriteAppend", false, "r") \ macro(DYN_AT_WRITE_APPEND_BACK, "atWriteAppendBack", false, "r") \ macro(DYN_CLEAR, "clear", false, "") \ diff --git a/src/V3AstNodeDType.h b/src/V3AstNodeDType.h index 74b3b3adc..37fd3d583 100644 --- a/src/V3AstNodeDType.h +++ b/src/V3AstNodeDType.h @@ -691,22 +691,24 @@ public: bool isCompound() const override { return false; } }; class AstCoverCrossDType final : public AstNodeDType { - // Borrowed pointer to VlCoverCrossT. + // Borrowed pointer to VlCoverCrossT<...> or construction-time VlCoverCrossDyn. const uint32_t m_dimensions; const uint32_t m_tuples; // Fixed capacity number of cross tuples (VlCoverCrossT's Tuples) const uint32_t m_bins; // Fixed capacity number of explicit bins (VlCoverCrossT's Bins) const uint32_t m_autoBins; // Fixed capacity number of auto bins (VlCoverCrossT's AutoBins) const uint64_t m_binWords; // Fixed capacity selection words (VlCoverCrossT's BinWords) + const bool m_dynamic; // Bin layout is determined at covergroup construction public: AstCoverCrossDType(FileLine* fl, uint32_t dimensions, uint32_t tuples, uint32_t bins, - uint32_t autoBins, uint64_t binWords) + uint32_t autoBins, uint64_t binWords, bool dynamic = false) : ASTGEN_SUPER_CoverCrossDType(fl) , m_dimensions{dimensions} , m_tuples{tuples} , m_bins{bins} , m_autoBins{autoBins} - , m_binWords{binWords} { + , m_binWords{binWords} + , m_dynamic{dynamic} { dtypep(this); } ASTGEN_MEMBERS_AstCoverCrossDType; @@ -714,11 +716,12 @@ public: BROKEN_RTN(m_dimensions == 0); return nullptr; } - bool sameNode(const AstNode* samep) const override { + bool sameNode(const AstNode* samep) const override { // LCOV_EXCL_START const AstCoverCrossDType* const sp = VN_DBG_AS(samep, CoverCrossDType); return dimensions() == sp->dimensions() && tuples() == sp->tuples() && bins() == sp->bins() - && autoBins() == sp->autoBins() && binWords() == sp->binWords(); - } + && autoBins() == sp->autoBins() && binWords() == sp->binWords() + && isDynamic() == sp->isDynamic(); + } // LCOV_EXCL_STOP bool similarDTypeNode(const AstNodeDType* samep) const override { return this == samep; } void dump(std::ostream& str) const override; void dumpJson(std::ostream& str) const override; @@ -728,6 +731,7 @@ public: uint32_t bins() const { return m_bins; } uint32_t autoBins() const { return m_autoBins; } uint64_t binWords() const { return m_binWords; } + bool isDynamic() const { return m_dynamic; } string cppTemplateArgs() const; AstBasicDType* basicp() const override VL_MT_STABLE { return nullptr; } int widthAlignBytes() const override { return sizeof(void*); } diff --git a/src/V3AstNodes.cpp b/src/V3AstNodes.cpp index 46d449c50..c5c177958 100644 --- a/src/V3AstNodes.cpp +++ b/src/V3AstNodes.cpp @@ -1224,6 +1224,7 @@ string AstCoverCrossDType::cppTemplateArgs() const { void AstCoverCrossDType::dump(std::ostream& str) const { Super::dump(str); str << " [" << cppTemplateArgs() << "]"; + if (isDynamic()) str << " [DYNAMIC]"; } void AstCoverCrossDType::dumpJson(std::ostream& str) const { dumpJsonNumFunc(str, dimensions); @@ -1231,6 +1232,7 @@ void AstCoverCrossDType::dumpJson(std::ostream& str) const { dumpJsonNumFunc(str, bins); dumpJsonNumFunc(str, autoBins); dumpJsonNumFunc(str, binWords); + dumpJsonBoolFuncIf(str, isDynamic); dumpJsonGen(str); } void AstCoverCrossDType::dumpSmall(std::ostream& str) const { @@ -2182,7 +2184,8 @@ AstNodeDType::CTypeRecursed AstNodeDType::cTypeRecurse(bool compound, bool packe info.m_type += ">"; } else if (const auto* const adtypep = VN_CAST(dtypep, CoverCrossDType)) { UASSERT_OBJ(!packed, this, "Unsupported type for packed struct or union"); - info.m_type = "VlCoverCrossT<" + adtypep->cppTemplateArgs() + ">*"; + info.m_type = adtypep->isDynamic() ? "VlCoverCrossDyn*" + : "VlCoverCrossT<" + adtypep->cppTemplateArgs() + ">*"; } else if (const auto* const adtypep = VN_CAST(dtypep, CoverpointDType)) { UASSERT_OBJ(!packed, this, "Unsupported type for packed struct or union"); info.m_type = "VlCoverpointT<" + cvtToStr(adtypep->hitBound()) + ">*"; diff --git a/src/V3Covergroup.cpp b/src/V3Covergroup.cpp index 83823c185..3434152e8 100644 --- a/src/V3Covergroup.cpp +++ b/src/V3Covergroup.cpp @@ -31,8 +31,8 @@ #include "V3File.h" #include "V3MemberMap.h" -#include #include +#include #include #include #include @@ -205,6 +205,10 @@ class FunctionalCoverageVisitor final : public VNVisitor { const VNUser1InUse m_inuser1; // STATE + std::set + m_runtimePoints; // Points needing value metadata and live-bin mapping + std::set m_runtimeCrosses; // Crosses over finalized live-bin dimensions + std::map m_excludedVars; // Sample-time state-exclusion flags AstClass* m_covergroupp = nullptr; // Current covergroup being processed AstClass* m_enclosingClassp = nullptr; // Class lexically enclosing the covergroup, if any AstVar* m_embeddedVarp = nullptr; // Embedded covergroup member of m_enclosingClassp, if any @@ -237,19 +241,23 @@ class FunctionalCoverageVisitor final : public VNVisitor { AstCoverBin* binp; // Declaration owning this Normal bin AstNodeExpr* valuep; // Individual array-bin value, or nullptr for a scalar bin }; + struct BinSpan final { + uint32_t first; // First Normal index of the bin declaration + uint32_t count; // Number of Normal bins of the declaration + uint32_t declared; // First runtime bin index, across all bin kinds + }; struct CoverpointBins final { uint32_t total = 0; // Number of Normal bins AstNodeExpr* exprp = nullptr; // Sampled expression, for the value domain std::vector values; // Values in runtime Normal-bin index order - std::vector excluded; // State ignore/illegal bins removing values - std::unordered_map> - spans; // Declared bin name -> first Normal index and number of bins + std::unordered_map spans; // Declared bin name -> index span }; std::map m_cpBins; // Runtime coverpoint -> binsof index ranges + std::vector m_detachedValues; // Array-bin values m_cpBins refers to std::set m_droppedCrosses; // Crosses with a bare-variable item: drop (COVERIGN) std::map m_cpDTypes; // Hit-list bound -> interned dtype - using CrossShape = std::tuple; + using CrossShape = std::tuple; std::map m_cxDTypes; AstVar* m_cgInstVarp = nullptr; // __Vcg_inst handle member of the current covergroup @@ -266,6 +274,9 @@ class FunctionalCoverageVisitor final : public VNVisitor { m_crossVars.clear(); m_cpVarMap.clear(); m_cpBins.clear(); + m_runtimePoints.clear(); + m_runtimeCrosses.clear(); + m_excludedVars.clear(); m_droppedCrosses.clear(); m_cgInstVarp = nullptr; @@ -285,6 +296,43 @@ class FunctionalCoverageVisitor final : public VNVisitor { } } + std::vector pending; + std::map> consumers; + std::map> inputs; + for (AstCoverpoint* const cpp : m_coverpoints) { + if (!cpp->exprp()->dtypep()->skipRefp()->isIntegralOrPacked()) continue; + // Bins without values leave the report (IEEE 1800-2023 19.11.1), exclusions or not. + if (!coverpointHasStateExclusions(cpp) && !coverpointHasEmptyBins(cpp)) continue; + m_runtimePoints.insert(cpp); + pending.push_back(cpp); + } + for (AstCoverCross* const crossp : m_coverCrosses) { + if (m_droppedCrosses.count(crossp)) continue; + for (AstNode* itemp = crossp->itemsp(); itemp; itemp = itemp->nextp()) { + const AstCoverpointRef* const refp = VN_AS(itemp, CoverpointRef); + if (refp->exprp()) continue; + const auto point = m_coverpointMap.find(refp->name()); + if (point != m_coverpointMap.end()) { + consumers[point->second].push_back(crossp); + inputs[crossp].push_back(point->second); + } + } + } + for (size_t next = 0; next < pending.size(); ++next) { + if (AstCoverpoint* const pointp = VN_CAST(pending[next], Coverpoint)) { + for (AstCoverCross* const crossp : consumers[pointp]) { + if (m_runtimeCrosses.emplace(crossp).second) pending.push_back(crossp); + } + } else { + for (AstCoverpoint* const pointp : inputs[VN_AS(pending[next], CoverCross)]) { + if (pointp->exprp()->dtypep()->skipRefp()->isIntegralOrPacked() + && m_runtimePoints.emplace(pointp).second) { + pending.push_back(pointp); + } + } + } + } + // The instance node owns this instance's coverpoint/cross runtimes, so it must exist // before any of them is created. Emitted first, ahead of both generate loops. generateInstanceAttach(); @@ -295,6 +343,16 @@ class FunctionalCoverageVisitor final : public VNVisitor { // For each cross, generate sampling code for (AstCoverCross* crossp : m_coverCrosses) generateCrossCode(crossp); + // Every cross has been built, so runtime points only need their exclusions from here. + for (AstCoverpoint* const cpp : m_coverpoints) { + if (!m_runtimePoints.count(cpp)) continue; + m_constructorp->addStmtsp(itemCall(cpp->fileline(), m_cpVarMap.at(cpp->name()), + VCMethod::COVERGROUP_VALUE_RELEASE) + ->makeStmt()); + } + for (AstNodeExpr* valuep : m_detachedValues) VL_DO_DANGLING(pushDeletep(valuep), valuep); + m_detachedValues.clear(); + // Generate coverage computation code (even for empty covergroups). Bin registration // with the coverage database is handled per coverpoint/cross by their runtime // registerBins() calls (emitted in generateCoverpoint/generateCross). @@ -310,6 +368,7 @@ class FunctionalCoverageVisitor final : public VNVisitor { static constexpr int COVER_BINS_LIMIT = 1000; // Sanity limit to avoid hangs from e.g. signed underflow + static constexpr size_t VALUE_LIST_ENTRIES = 256; // Metadata entries per constructor call void expandAutomaticBins(AstCoverpoint* coverpointp, AstNodeExpr* exprp) { // Find and expand any automatic bins @@ -433,156 +492,50 @@ class FunctionalCoverageVisitor final : public VNVisitor { } } - // Extract individual values from a range expression list, used only to carve values - // out of implicit auto-bins. Iterates over all siblings (nextp) in the list, handling - // AstConst (single value) and AstInsideRange ([lo:hi]); an open-ended bound ('$', - // AstUnbounded) resolves to the coverpoint domain min (lower) or max (upper, == maxVal). - void extractValuesFromRange(AstNode* nodep, std::set& values, uint64_t maxVal) { - // Cap enumeration so a '$'-bounded or otherwise huge range cannot blow up memory; - // auto-bins are per-value only for small domains, so a partial set is harmless here. - constexpr size_t maxEnumerate = 1ULL << 16; - for (AstNode* np = nodep; np; np = np->nextp()) { - np = V3Const::constifyEdit(np); - if (AstConst* constp = VN_CAST(np, Const)) { - if (constp->num().isFourState()) - continue; // wildcard patterns can't be enumerated - values.insert(constp->toUQuad()); - } else if (AstInsideRange* rangep = VN_CAST(np, InsideRange)) { - AstNodeExpr* const lhsp = V3Const::constifyEdit(rangep->lhsp()); - AstNodeExpr* const rhsp = V3Const::constifyEdit(rangep->rhsp()); - const bool loUnbounded = VN_IS(lhsp, Unbounded); - const bool hiUnbounded = VN_IS(rhsp, Unbounded); - AstConst* const loConstp = VN_CAST(lhsp, Const); - AstConst* const hiConstp = VN_CAST(rhsp, Const); - if ((!loConstp && !loUnbounded) || (!hiConstp && !hiUnbounded)) { - rangep->v3error("Non-constant expression in bin range; " - "range bounds must be constants (IEEE 1800-2023 19.5)"); - continue; - } - if ((loConstp && loConstp->num().isFourState()) - || (hiConstp && hiConstp->num().isFourState())) - continue; - const uint64_t lo = loUnbounded ? 0 : loConstp->toUQuad(); - const uint64_t hi = hiUnbounded ? maxVal : hiConstp->toUQuad(); - for (uint64_t v = lo; v <= hi; v++) { - if (values.size() >= maxEnumerate) break; - values.insert(v); - } - } else { - np->v3error("Non-constant expression in bin value list; values must be constants " - "(IEEE 1800-2023 19.5)"); - } - } - } - - // Single-pass categorization: determine whether any regular (non-ignore/illegal) bins exist - // and collect the set of excluded values from ignore/illegal bins. - void categorizeBins(AstCoverpoint* coverpointp, bool& hasRegularOut, - std::set& excludedOut, uint64_t maxVal) { - hasRegularOut = false; - for (AstNode* binp = coverpointp->binsp(); binp; binp = binp->nextp()) { - AstCoverBin* const cbinp = VN_AS(binp, CoverBin); - const VCoverBinsType btype = cbinp->binsType(); - if (btype == VCoverBinsType::BINS_IGNORE || btype == VCoverBinsType::BINS_ILLEGAL) { - if (AstNode* rangep = cbinp->rangesp()) { - extractValuesFromRange(rangep, excludedOut, maxVal); - } - } else { - hasRegularOut = true; - } - } - } - - // Create implicit automatic bins when coverpoint has no explicit regular bins + // IEEE 1800-2023 19.5.3/19.11.1: partition first, then apply exclusions. void createImplicitAutoBins(AstCoverpoint* coverpointp, AstNodeExpr* exprp, int autoBinMax) { - const int width = exprp->width(); - const uint64_t maxVal = (width >= 64) ? UINT64_MAX : ((1ULL << width) - 1); - - // Single pass: check for regular bins and collect excluded values simultaneously. - // maxVal resolves any '$' (open-ended) bound in ignore_bins/illegal_bins ranges. - bool hasRegular = false; - std::set excluded; - categorizeBins(coverpointp, hasRegular, excluded, maxVal); - - // If already has regular bins, nothing to do - if (hasRegular) return; - - UINFO(4, " Creating implicit automatic bins for coverpoint: " << coverpointp->name()); - - const uint64_t numTotalValues = (width >= 64) ? UINT64_MAX : (1ULL << width); - const uint64_t numValidValues = numTotalValues - excluded.size(); - - // Determine number of bins to create (based on non-excluded values) - int numBins; - if (numValidValues <= static_cast(autoBinMax)) { - // Create one bin per valid value - numBins = numValidValues; - } else { - // Create autoBinMax bins, dividing range - numBins = autoBinMax; + for (AstNode* nodep = coverpointp->binsp(); nodep; nodep = nodep->nextp()) { + const VCoverBinsType kind = VN_AS(nodep, CoverBin)->binsType(); + if (kind != VCoverBinsType::BINS_IGNORE && kind != VCoverBinsType::BINS_ILLEGAL) + return; } - - UINFO(4, " Width=" << width << " numTotalValues=" << numTotalValues - << " numValidValues=" << numValidValues << " autoBinMax=" - << autoBinMax << " creating " << numBins << " bins"); - - // Strategy: Create bins for each value (if numValidValues <= autoBinMax) - // or create range bins that avoid excluded values - if (numValidValues <= static_cast(autoBinMax)) { - // Create one bin per valid value - int binCount = 0; - for (uint64_t v = 0; v <= maxVal && binCount < numBins; v++) { - // Skip excluded values - if (excluded.find(v) != excluded.end()) continue; - - // Create single-value bin - AstConst* const valConstp = new AstConst{ - coverpointp->fileline(), V3Number(coverpointp->fileline(), width, v)}; - AstConst* const valConstp2 = new AstConst{ - coverpointp->fileline(), V3Number(coverpointp->fileline(), width, v)}; - - AstInsideRange* const rangep - = new AstInsideRange{coverpointp->fileline(), valConstp, valConstp2}; - rangep->dtypeFrom(exprp); - - const string binName = "auto_" + std::to_string(binCount); - AstCoverBin* const newBinp - = new AstCoverBin{coverpointp->fileline(), binName, rangep, false, false}; - - coverpointp->addBinsp(newBinp); - binCount++; + const int width = exprp->width(); + const int arithmeticWidth = width + 1; + V3Number total{coverpointp, arithmeticWidth}; + total.setBit(width, 1); + const uint32_t count = width < 31 ? std::min(uint32_t{1} << width, autoBinMax) + : static_cast(autoBinMax); + if (!count) return; + V3Number divisor{coverpointp, arithmeticWidth, count}; + V3Number stride{coverpointp, arithmeticWidth}; + stride.opDiv(total, divisor); + const CrossValueRange domain + = crossValueDomain(coverpointp, width, exprp->isSigned(), arithmeticWidth); + const V3Number one{coverpointp, arithmeticWidth, 1}; + for (uint32_t bin = 0; bin < count; ++bin) { + const V3Number ordinal{coverpointp, arithmeticWidth, bin}; + const V3Number nextOrdinal{coverpointp, arithmeticWidth, bin + 1}; + V3Number low{coverpointp, arithmeticWidth}; + V3Number high{coverpointp, arithmeticWidth}; + low.opMul(stride, ordinal); + if (bin + 1 == count) { + high = total; + } else { + high.opMul(stride, nextOrdinal); } - UINFO(4, " Created " << binCount << " single-value automatic bins"); - } else { - // Create range bins (more complex - need to handle excluded values in ranges) - // For simplicity, create bins and let excluded values not match any bin - const uint64_t binSize = (maxVal + 1) / numBins; - - for (int i = 0; i < numBins; i++) { - const uint64_t lo = i * binSize; - const uint64_t hi = (i == numBins - 1) ? maxVal : ((i + 1) * binSize - 1); - - // Create constants for range - AstConst* const loConstp = new AstConst{ - coverpointp->fileline(), V3Number(coverpointp->fileline(), width, lo)}; - AstConst* const hiConstp = new AstConst{ - coverpointp->fileline(), V3Number(coverpointp->fileline(), width, hi)}; - - // Create InsideRange [lo:hi] - AstInsideRange* const rangep - = new AstInsideRange{coverpointp->fileline(), loConstp, hiConstp}; - rangep->dtypeFrom(exprp); - - // Create bin name - const string binName = "auto_" + std::to_string(i); - AstCoverBin* const newBinp - = new AstCoverBin{coverpointp->fileline(), binName, rangep, false, false}; - - // Add to coverpoint - coverpointp->addBinsp(newBinp); - } - - UINFO(4, " Created range-based automatic bins"); + V3Number adjusted{coverpointp, arithmeticWidth}; + adjusted.opSub(high, one); + high = adjusted; + adjusted.opAdd(low, domain.lo); + low = adjusted; + adjusted.opAdd(high, domain.lo); + high = adjusted; + AstConst* const lop = newValueConst(coverpointp->fileline(), low, exprp); + AstConst* const hip = newValueConst(coverpointp->fileline(), high, exprp); + AstInsideRange* const rangep = new AstInsideRange{coverpointp->fileline(), lop, hip}; + rangep->dtypeFrom(exprp); + coverpointp->addBinsp(new AstCoverBin{coverpointp->fileline(), "auto_" + cvtToStr(bin), + rangep, false, false}); } } @@ -720,6 +673,39 @@ class FunctionalCoverageVisitor final : public VNVisitor { //==================================================================== // VlCoverpoint conversion + static bool coverpointHasStateExclusions(const AstCoverpoint* coverpointp) { + for (const AstNode* nodep = coverpointp->binsp(); nodep; nodep = nodep->nextp()) { + const AstCoverBin* const binp = VN_AS(nodep, CoverBin); + if (!binp->transp() && binp->rangesp() + && (binp->binsType() == VCoverBinsType::BINS_IGNORE + || binp->binsType() == VCoverBinsType::BINS_ILLEGAL)) { + return true; + } + } + return false; + } + + // True if a Normal state bin, or an array-bin element, has no value of the coverpoint's + // type (IEEE 1800-2023 19.5.7). Array ranges enumerate in-type values, so cannot vanish. + static bool coverpointHasEmptyBins(const AstCoverpoint* coverpointp) { + AstNodeExpr* const exprp = coverpointp->exprp(); + for (AstNode* nodep = coverpointp->binsp(); nodep; nodep = nodep->nextp()) { + const AstCoverBin* const binp = VN_AS(nodep, CoverBin); + if (!binp->binsType().binIsNormal() || binp->transp() || !binp->rangesp()) continue; + bool empty = true; + for (AstNode* valuep = binp->rangesp(); valuep; valuep = valuep->nextp()) { + if (binp->isArray() && VN_IS(valuep, InsideRange)) continue; + CrossValueRange range{valuep, resolveWidth(valuep, exprp)}; + const bool none = resolveValue(valuep, exprp, true, binp->isWildcard(), range) + && crossRangeEmpty(range); + if (binp->isArray() && none) return true; + empty &= none; + } + if (empty && !binp->isArray()) return true; + } + return false; + } + // True if a coverpoint has any transition bin. Used to decide whether sample() emits the // end-of-sample previous-value update that transition matching needs. static bool coverpointHasTransition(AstCoverpoint* coverpointp) { @@ -1022,23 +1008,19 @@ class FunctionalCoverageVisitor final : public VNVisitor { return values; } - // Emit a 'this->m_cp->addSingleNamer/addArrayNamer(...)' statement for one bin - AstNodeStmt* makeNamer(AstVar* cpVarp, AstCoverBin* binp, int count, + // Emit a 'this->m_cp->addSingleNamer/addArrayNamer(...)' statement for one bin whose first + // runtime bin index is 'declared' + AstNodeStmt* makeNamer(AstVar* cpVarp, AstCoverBin* binp, int count, uint32_t declared, const std::vector& values = {}) { FileLine* const fl = binp->fileline(); CoverpointBins& bins = m_cpBins.at(cpVarp); const uint32_t normalCount = binp->binsType().binIsNormal() ? static_cast(count < 0 ? 1 : count) : 0; - bins.spans.emplace(binp->name(), std::make_pair(bins.total, normalCount)); + bins.spans.emplace(binp->name(), BinSpan{bins.total, normalCount, declared}); bins.total += normalCount; for (uint32_t i = 0; i < normalCount; ++i) { bins.values.push_back({binp, values.empty() ? nullptr : values[i]}); } - if (!binp->transp() - && (binp->binsType() == VCoverBinsType::BINS_IGNORE - || binp->binsType() == VCoverBinsType::BINS_ILLEGAL)) { - bins.excluded.push_back(binp); - } // Under --protect-ids the filename and bin name flow into the coverage database // verbatim, so obfuscate them exactly as line/toggle coverage points are (whole- // unit filename, per-word bin name). A no-op when --protect-ids is off. @@ -1085,6 +1067,13 @@ class FunctionalCoverageVisitor final : public VNVisitor { + coverpointp->prettyNameQ())); } if (binp->iffp()) condp = new AstLogAnd{fl, binp->iffp()->cloneTree(false), condp}; + const auto excluded = m_excludedVars.find(cpVarp); + if (excluded != m_excludedVars.end() && !binp->transp() + && (binp->binsType().binIsNormal() + || binp->binsType() == VCoverBinsType::BINS_DEFAULT)) { + condp = new AstLogAnd{ + fl, new AstNot{fl, new AstVarRef{fl, excluded->second, VAccess::READ}}, condp}; + } AstNodeExpr* const guardedp = applyCoverpointIffCondition(coverpointp, fl, condp); UASSERT_OBJ(m_sampleFuncp, binp, "sample() CFunc not set for coverpoint"); m_sampleFuncp->addStmtsp(new AstIf{fl, guardedp, actionp, nullptr}); @@ -1111,6 +1100,7 @@ class FunctionalCoverageVisitor final : public VNVisitor { // increments, the constructor configuration (init + namers), and registration. void generateCoverpoint(AstCoverpoint* coverpointp, AstNodeExpr* exprp, int atLeastValue) { FileLine* const fl = coverpointp->fileline(); + const bool dynamic = m_runtimePoints.count(coverpointp); UINFO(4, " Generating VlCoverpoint member: " << coverpointp->name()); if (AstNodeExpr* const iffp = coverpointp->iffp()) { @@ -1140,14 +1130,33 @@ class FunctionalCoverageVisitor final : public VNVisitor { m_sampleFuncp->addStmtsp( itemCall(fl, cpVarp, VCMethod::COVERGROUP_CLEAR_HIT_LIST)->makeStmt()); } + if (dynamic && coverpointHasStateExclusions(coverpointp)) { + AstVar* const excludedp + = new AstVar{fl, VVarType::BLOCKTEMP, + "__VcpExcluded_" + sanitizeGeneratedName(coverpointp->name()), + coverpointp->findBitDType()}; + excludedp->funcLocal(true); + m_sampleFuncp->addStmtsp(excludedp); + AstCMethodHard* const callp + = itemCall(fl, cpVarp, + exprp->isWide() ? VCMethod::COVERGROUP_VALUE_EXCLUDED_W + : VCMethod::COVERGROUP_VALUE_EXCLUDED, + {exprp->cloneTree(false)}); + callp->dtypeSetBit(); + m_sampleFuncp->addStmtsp( + new AstAssign{fl, new AstVarRef{fl, excludedp, VAccess::WRITE}, callp}); + m_excludedVars.emplace(cpVarp, excludedp); + } // Walk bins (non-default, then default), assigning sequential indices that match the // namer append order; emit sample increments and collect namer statements. std::vector namerStmts; std::vector defaultBins; + std::vector> metadata; int idx = 0; for (AstNode* binp = coverpointp->binsp(); binp; binp = binp->nextp()) { AstCoverBin* const cbinp = VN_AS(binp, CoverBin); + const int errorsBefore = dynamic ? V3Error::errorCount() : 0; if (cbinp->binsType() == VCoverBinsType::BINS_DEFAULT) { defaultBins.push_back(cbinp); continue; @@ -1159,11 +1168,14 @@ class FunctionalCoverageVisitor final : public VNVisitor { // them as one runtime bin incremented by any matching sequence. The sequence // matching is generated as a state machine, with the hit routed to this bin's // runtime slot. - namerStmts.push_back(makeNamer(cpVarp, cbinp, -1)); + namerStmts.push_back(makeNamer(cpVarp, cbinp, -1, static_cast(idx))); const ConvBinTarget tgt{cpVarp, idx, cbinp->binsType().binIsNormal()}; for (AstNode* sp = cbinp->transp(); sp; sp = sp->nextp()) generateSingleTransitionCode(coverpointp, cbinp, exprp, tgt, VN_AS(sp, CoverTransSet)); + if (dynamic && V3Error::errorCount() == errorsBefore) { + metadata.emplace_back(cbinp, idx, nullptr); + } ++idx; continue; } @@ -1171,26 +1183,32 @@ class FunctionalCoverageVisitor final : public VNVisitor { bool unsupported = false; std::vector values = extractArrayValues(cbinp, exprp, unsupported); if (unsupported) continue; // bin ignored (COVERIGN emitted); reserve no slot - namerStmts.push_back( - makeNamer(cpVarp, cbinp, static_cast(values.size()), values)); + namerStmts.push_back(makeNamer(cpVarp, cbinp, static_cast(values.size()), + static_cast(idx), values)); for (AstNodeExpr* valuep : values) { - // TODO: A 4-state bin value (e.g. bins b[] = {2'b0x}) must match with === - // (AstEqCase) per IEEE 1800-2023 19.5.4. == is equivalent under 2-state sim - // (x/z collapse to 0); switch to AstEqCase when 4-state sim support lands. - emitConvHitIf(coverpointp, cbinp, cpVarp, idx++, - new AstEq{cbinp->fileline(), exprp->cloneTree(false), valuep}); + // The cross selections of this covergroup still read the value. + m_detachedValues.push_back(valuep); + emitConvHitIf(coverpointp, cbinp, cpVarp, idx, + buildValueCondition(cbinp, exprp, valuep)); + if (dynamic && V3Error::errorCount() == errorsBefore) { + metadata.emplace_back(cbinp, idx, valuep); + } + ++idx; } } else { - namerStmts.push_back(makeNamer(cpVarp, cbinp, -1)); + namerStmts.push_back(makeNamer(cpVarp, cbinp, -1, static_cast(idx))); // buildBinCondition is null for 'ignore_bins = default' (no ranges); the bin // still gets a reserved slot (recorded, never incremented). if (AstNodeExpr* const condp = buildBinCondition(cbinp, exprp)) emitConvHitIf(coverpointp, cbinp, cpVarp, idx, condp); + if (dynamic && V3Error::errorCount() == errorsBefore) { + metadata.emplace_back(cbinp, idx, nullptr); + } ++idx; } } for (AstCoverBin* const defBinp : defaultBins) { - namerStmts.push_back(makeNamer(cpVarp, defBinp, -1)); + namerStmts.push_back(makeNamer(cpVarp, defBinp, -1, static_cast(idx))); emitConvHitIf(coverpointp, defBinp, cpVarp, idx++, buildDefaultCondition(coverpointp, exprp, defBinp->fileline())); } @@ -1219,6 +1237,22 @@ class FunctionalCoverageVisitor final : public VNVisitor { cnum(fl, static_cast(idx))}) ->makeStmt()); for (AstNodeStmt* const ns : namerStmts) m_constructorp->addStmtsp(ns); + if (dynamic) { + m_constructorp->addStmtsp( + itemCall(fl, cpVarp, VCMethod::COVERGROUP_VALUE_TYPE, + {cnum(fl, exprp->width()), cnum(fl, exprp->isSigned())}) + ->makeStmt()); + ValueLists lists; + for (const auto& entry : metadata) { + collectValueMetadata(lists, exprp, std::get<0>(entry), std::get<1>(entry), + std::get<2>(entry)); + } + emitValueList(fl, cpVarp, VCMethod::COVERGROUP_VALUE_RANGES, lists.m_ranges); + emitValueList(fl, cpVarp, VCMethod::COVERGROUP_VALUE_PATTERNS, lists.m_patterns); + emitValueList(fl, cpVarp, VCMethod::COVERGROUP_VALUE_TRANSITIONS, lists.m_transitions); + m_constructorp->addStmtsp( + itemCall(fl, cpVarp, VCMethod::COVERGROUP_VALUE_FINALIZE)->makeStmt()); + } if (v3Global.opt.coverage()) { const std::string page = VIdProtect::protectIf("v_covergroup/" + m_covergroupp->name(), prot); @@ -1357,6 +1391,23 @@ class FunctionalCoverageVisitor final : public VNVisitor { return errorp; } + // Preserve the coverpoint's width and signedness after V3Width. + static AstConst* newValueConst(FileLine* fl, const V3Number& value, const AstNodeExpr* exprp) { + V3Number narrowed{fl, exprp->width(), 0}; + narrowed.opAssign(value); + AstConst* const constp = new AstConst{fl, narrowed}; + constp->dtypeFrom(exprp); + return constp; + } + + // A real copy of a real or integral range bound, for comparing with a real coverpoint. + static AstConst* newRealConst(AstConst* constp) { + if (constp->num().isDouble()) return constp->cloneTree(false); + V3Number real{&constp->num(), 64}; + real.opIToRD(constp->num(), constp->isSigned()); + return new AstConst{constp->fileline(), real}; + } + // Clone a constant node, widening to targetWidth if needed (zero-extend). // Used to ensure comparisons use matching widths after V3Width has run. static AstConst* widenConst(FileLine* fl, AstConst* constp, int targetWidth) { @@ -1367,42 +1418,51 @@ class FunctionalCoverageVisitor final : public VNVisitor { } // Build a range condition: minp <= exprp <= maxp. - // Uses signed comparisons if exprp is signed; omits trivially-true bounds for unsigned. + // Uses signed comparisons if exprp is signed; omits trivially-true domain bounds. // All arguments are non-owning; clones exprp/minp/maxp as needed. AstNodeExpr* makeRangeCondition(FileLine* fl, AstNodeExpr* exprp, AstNodeExpr* minp, AstNodeExpr* maxp) { const int exprWidth = exprp->widthMin(); AstConst* const minConstp = VN_AS(minp, Const); AstConst* const maxConstp = VN_AS(maxp, Const); + if (exprp->isDouble()) { + // A real coverpoint has no finite domain bounds to omit. + return new AstAnd{fl, + new AstGteD{fl, exprp->cloneTree(false), newRealConst(minConstp)}, + new AstLteD{fl, exprp->cloneTree(false), newRealConst(maxConstp)}}; + } // Widen constants to match expression width so post-V3Width nodes use correct macros AstConst* const minWidep = widenConst(fl, minConstp, exprWidth); AstConst* const maxWidep = widenConst(fl, maxConstp, exprWidth); + V3Number minimum{fl, exprWidth, 0}; + V3Number maximum{fl, exprWidth, 0}; + maximum.setAllBits1(); if (exprp->isSigned()) { - return new AstAnd{fl, new AstGteS{fl, exprp->cloneTree(false), minWidep}, - new AstLteS{fl, exprp->cloneTree(false), maxWidep}}; + minimum.setBit(exprWidth - 1, 1); + maximum.setBit(exprWidth - 1, 0); } - // Unsigned: skip bounds that are trivially satisfied for the expression width - const bool skipLowerCheck = (minConstp->toUQuad() == 0); - bool skipUpperCheck = false; - if (exprWidth <= 64) { - const uint64_t maxVal - = (exprWidth == 64) ? ~static_cast(0) : ((1ULL << exprWidth) - 1ULL); - skipUpperCheck = (maxConstp->toUQuad() == maxVal); - } - if (skipLowerCheck && skipUpperCheck) { + AstNodeExpr* lowerp = nullptr; + AstNodeExpr* upperp = nullptr; + if (minWidep->num().isCaseEq(minimum)) { VL_DO_DANGLING(pushDeletep(minWidep), minWidep); - VL_DO_DANGLING(pushDeletep(maxWidep), maxWidep); - return new AstConst{fl, AstConst::BitTrue{}}; - } else if (skipLowerCheck) { - VL_DO_DANGLING(pushDeletep(minWidep), minWidep); - return new AstLte{fl, exprp->cloneTree(false), maxWidep}; - } else if (skipUpperCheck) { - VL_DO_DANGLING(pushDeletep(maxWidep), maxWidep); - return new AstGte{fl, exprp->cloneTree(false), minWidep}; } else { - return new AstAnd{fl, new AstGte{fl, exprp->cloneTree(false), minWidep}, - new AstLte{fl, exprp->cloneTree(false), maxWidep}}; + lowerp = exprp->isSigned() ? static_cast( + new AstGteS{fl, exprp->cloneTree(false), minWidep}) + : static_cast( + new AstGte{fl, exprp->cloneTree(false), minWidep}); } + if (maxWidep->num().isCaseEq(maximum)) { + VL_DO_DANGLING(pushDeletep(maxWidep), maxWidep); + } else { + upperp = exprp->isSigned() ? static_cast( + new AstLteS{fl, exprp->cloneTree(false), maxWidep}) + : static_cast( + new AstLte{fl, exprp->cloneTree(false), maxWidep}); + } + if (lowerp && upperp) return new AstAnd{fl, lowerp, upperp}; + if (lowerp) return lowerp; + if (upperp) return upperp; + return new AstConst{fl, AstConst::BitTrue{}}; } // Build a one-sided comparison for an open-ended bin range whose other bound is '$'. @@ -1569,11 +1629,18 @@ class FunctionalCoverageVisitor final : public VNVisitor { std::vector strides; // Flat-index stride per dimension bool valid = true; // False if this explicit bin cannot be implemented }; + struct CrossBinsofTarget final { + const CoverpointBins* m_binsp = nullptr; // Declared bins; null after a reported error + uint32_t m_dimension = 0; // Coverpoint index within the cross + uint32_t m_first = 0; // First declared normal bin in the selected span + uint32_t m_count = 0; // Number of declared normal bins in the selected span + uint32_t m_declaredFirst = 0; // First runtime bin index of the selected span + uint32_t m_declaredEnd = UINT32_MAX; // One past the last runtime bin index + }; struct CrossValueRange final { V3Number lo; // Inclusive lower bound, sign-extended to the comparison width V3Number hi; // Inclusive upper bound V3Number pattern; // Allowed bit values; all X for an ordinary interval - bool singleton = false; // A single value, possibly a wildcard pattern bool wildcard = false; // A wildcard singleton rather than an exact four-state value CrossValueRange(AstNode* nodep, int width) @@ -1616,8 +1683,32 @@ class FunctionalCoverageVisitor final : public VNVisitor { return !result.opLtS(lhs, rhs).isEqZero(); } + // Round a real bound inward to the nearest coverpoint value (IEEE 1800-2023 19.5.7). Sets + // 'empty' if the domain has no value on the bound's side. + static void crossRealBound(const AstConst* constp, AstNodeExpr* exprp, bool upper, + const CrossValueRange& domain, V3Number& result, bool& empty) { + const double value = constp->num().toDouble(); + const double bound = upper ? std::floor(value) : std::ceil(value); + // Powers of two are exact, so the integral bound compares exactly with the domain edges. + const double limit + = std::ldexp(1.0, exprp->isSigned() ? exprp->width() - 1 : exprp->width()); + const double minimum = exprp->isSigned() ? -limit : 0.0; + if (std::isnan(bound) || (upper ? bound < minimum : bound >= limit)) { + empty = true; + result = domain.lo; + } else if (bound < minimum) { + result = domain.lo; + } else if (bound >= limit) { + result = domain.hi; + } else { + V3Number real{&result, 64}; + real.setDouble(bound); + result.opRToIRoundS(real); + } + } + static bool crossRangeBound(AstNode* nodep, AstNodeExpr* exprp, bool upper, bool binValue, - V3Number& result) { + const CrossValueRange& domain, V3Number& result, bool& empty) { if (VN_IS(nodep, Unbounded)) { V3Number limit{nodep, exprp->width()}; if (upper) limit.setAllBits1(); @@ -1630,7 +1721,12 @@ class FunctionalCoverageVisitor final : public VNVisitor { return true; } const AstConst* const constp = VN_CAST(nodep, Const); - if (!constp || constp->num().isOpaque()) return false; + if (!constp) return false; + if (constp->num().isDouble()) { + crossRealBound(constp, exprp, upper, domain, result, empty); + return true; + } + if (constp->num().isString()) return false; if (binValue && exprp->isSigned() && constp->width() <= exprp->width()) { // Bin bit patterns use the coverpoint's effective type (IEEE 1800-2023 19.5.7). // Wider values and intersect filters retain their values for domain clipping. @@ -1674,37 +1770,88 @@ class FunctionalCoverageVisitor final : public VNVisitor { static bool crossValueRange(AstNode* nodep, AstNodeExpr* exprp, bool binValue, bool wildcard, const CrossValueRange& domain, CrossValueRange& range) { + bool empty = false; + const AstConst* const constp = VN_CAST(nodep, Const); if (const AstInsideRange* const rangep = VN_CAST(nodep, InsideRange)) { - if (!crossRangeBound(rangep->lhsp(), exprp, false, binValue, range.lo) - || !crossRangeBound(rangep->rhsp(), exprp, true, binValue, range.hi) + if (!crossRangeBound(rangep->lhsp(), exprp, false, binValue, domain, range.lo, empty) + || !crossRangeBound(rangep->rhsp(), exprp, true, binValue, domain, range.hi, empty) || range.lo.isFourState() || range.hi.isFourState()) { return false; } + } else if (constp && constp->num().isDouble()) { + // A real value participates only if integral; it has no wildcard bits. + crossRealBound(constp, exprp, false, domain, range.lo, empty); + crossRealBound(constp, exprp, true, domain, range.hi, empty); } else { - range.singleton = true; - if (!crossRangeBound(nodep, exprp, false, binValue, range.lo)) return false; + if (!crossRangeBound(nodep, exprp, false, binValue, domain, range.lo, empty)) { + return false; + } + if (binValue && exprp->isSigned() && constp && !constp->isSigned() + && constp->width() > exprp->width()) { + bool representable = true; + for (int bit = exprp->width(); bit < constp->width(); ++bit) { + representable &= !constp->num().bitIs1(bit); + } + if (representable) { + V3Number value{nodep, exprp->width()}; + value.opAssign(constp->num()); + range.lo.opExtendS(value, value.width()); + } + } range.hi = range.lo; range.wildcard = wildcard; if (wildcard) { range.pattern = range.lo; range.lo = domain.lo; range.hi = domain.hi; - if (const AstConst* const constp = VN_CAST(nodep, Const)) { - if (constp->isSigned()) { - // Replicated X sign bits are correlated, not independent wildcards. - // The source domain preserves expansion-before-casting (19.5.7). - intersectCrossRange(range, crossValueDomain(nodep, constp->width(), true, - domain.lo.width())); - } + if (constp && constp->isSigned()) { + // Replicated X sign bits are correlated, not independent wildcards. + // The source domain preserves expansion-before-casting (19.5.7). + intersectCrossRange( + range, crossValueDomain(nodep, constp->width(), true, domain.lo.width())); } } } + if (empty) { + range.lo = domain.hi; + range.hi = domain.lo; + return true; + } if (!range.lo.isFourState()) intersectCrossRange(range, domain); + if (range.wildcard && exprp->isSigned()) { + const int sign = exprp->width() - 1; + if (constp && !constp->isSigned() && constp->width() > exprp->width()) { + // Unsigned equality preserves every target bit pattern if discarded bits are zero. + for (int bit = exprp->width(); bit < constp->width(); ++bit) { + if (constp->num().bitIs1(bit)) { + range.lo = domain.hi; + range.hi = domain.lo; + return true; + } + } + for (int bit = exprp->width(); bit < range.pattern.width(); ++bit) { + if (range.pattern.bitIsXZ(sign)) + range.pattern.setBit(bit, 'x'); + else + range.pattern.setBit(bit, range.pattern.bitIs1(sign)); + } + return true; + } + // Discarded high bits constrain the target sign, rather than becoming don't-cares. + for (int bit = exprp->width(); bit < range.pattern.width(); ++bit) { + if (range.pattern.bitIsXZ(bit)) continue; + const bool value = range.pattern.bitIs1(bit); + if (!range.pattern.bitIsXZ(sign) && range.pattern.bitIs1(sign) != value) { + range.lo = domain.hi; + range.hi = domain.lo; + break; + } + range.pattern.setBit(sign, value); + } + } return true; } - enum class CrossMatchResult : uint8_t { MATCH, NO_MATCH, WORK_LIMIT }; - enum CrossRangeState : uint8_t { CROSS_INSIDE_BOUNDS = 0, // Prefix is strictly inside the interval CROSS_AT_LOWER = 1, @@ -1712,8 +1859,6 @@ class FunctionalCoverageVisitor final : public VNVisitor { CROSS_AT_BOUNDS = CROSS_AT_LOWER | CROSS_AT_UPPER, CROSS_NO_MATCH = 4 // Prefix cannot match the interval/pattern }; - static constexpr size_t CROSS_MATCH_LINEAR_ALLOWANCE = 4; // Minimum linear traversals - static constexpr size_t CROSS_MATCH_WORK_LIMIT = 1U << 20; // Base bit-step budget per search static CrossRangeState crossRangeStep(const CrossValueRange& range, CrossRangeState state, int bit, int value) { @@ -1751,133 +1896,35 @@ class FunctionalCoverageVisitor final : public VNVisitor { return states != 0; } - static std::array crossFreeBelow(const CrossValueRange& range) { - const int width = range.pattern.width(); - int fixed = width; - int lower = width; - int upper = width; - for (int bit = 0; bit < width; ++bit) { - if (fixed == width && !range.pattern.bitIsXZ(bit)) fixed = bit; - if (lower == width && range.lo.bitIs1(bit)) lower = bit; - if (upper == width && !range.hi.bitIs1(bit)) upper = bit; - } - return {fixed, std::min(fixed, lower), std::min(fixed, upper), - std::min({fixed, lower, upper})}; + // True if no coverpoint value participates in a resolved value or range. A value with x + // or z bits participates only as a wildcard pattern (IEEE 1800-2023 19.5.7). + static bool crossRangeEmpty(const CrossValueRange& range) { + if (range.lo.isFourState()) return true; + return crossValueLess(range.hi, range.lo) + || (range.wildcard && !crossWildcardIntersects(range)); } - static bool crossRangeContains(const CrossValueRange& range, const V3Number& value) { - if (range.lo.isFourState() || crossValueLess(value, range.lo) - || crossValueLess(range.hi, value)) { - return false; - } - V3Number result{&value}; - return !result.opWildEq(value, range.pattern).isEqZero(); + // Comparison width that holds both the coverpoint's and a bin or intersect value's range. + static int resolveWidth(AstNode* nodep, const AstNodeExpr* exprp) { + return std::max(exprp->width(), crossRangeWidth(nodep)) + 1; } - static CrossMatchResult crossOutsideExcluded(const CrossValueRange& range, - const std::vector& excluded) { - // Array-bin elements and singleton filters need no prefix search. - if (range.lo.isCaseEq(range.hi)) { - if (!crossRangeContains(range, range.lo)) return CrossMatchResult::NO_MATCH; - return std::none_of(excluded.begin(), excluded.end(), - [&](const CrossValueRange& exclusion) { - return crossRangeContains(exclusion, range.lo); - }) - ? CrossMatchResult::MATCH - : CrossMatchResult::NO_MATCH; - } - std::vector blockers; - std::vector> freeBelow; - for (const CrossValueRange& exclusion : excluded) { - if (exclusion.lo.isFourState() || crossValueLess(exclusion.hi, exclusion.lo) - || crossValueLess(exclusion.hi, range.lo) - || crossValueLess(range.hi, exclusion.lo)) { - continue; - } - blockers.push_back(&exclusion); - freeBelow.push_back(crossFreeBelow(exclusion)); - } - if (blockers.empty()) { - return !range.wildcard || crossWildcardIntersects(range) ? CrossMatchResult::MATCH - : CrossMatchResult::NO_MATCH; - } - - struct Frame final { - int bit; // Next bit to assign - std::vector state; // Bound states for candidate and exclusions - int nextValue = 0; // Next bit value to try - }; - std::vector stack{ - {range.pattern.width() - 1, - std::vector(blockers.size() + 1, CROSS_AT_BOUNDS), 0}}; - std::set>> failed; - size_t work = 0; - const size_t stepCost = blockers.size() + 1; - const size_t workLimit - = std::max(CROSS_MATCH_WORK_LIMIT, static_cast(range.pattern.width()) - * stepCost * CROSS_MATCH_LINEAR_ALLOWANCE); - // Seek one witness, pruning prefixes wholly covered by an exclusion. Memoizing - // failed prefixes avoids repeated work; a budget bounds hard wildcard unions. - while (!stack.empty()) { - Frame& frame = stack.back(); - if (frame.nextValue == 2) { - failed.emplace(frame.bit, std::move(frame.state)); - stack.pop_back(); - continue; - } - if (stepCost > workLimit - work) return CrossMatchResult::WORK_LIMIT; - work += stepCost; - const int value = frame.nextValue++; - const CrossRangeState candidate - = crossRangeStep(range, frame.state[0], frame.bit, value); - if (candidate == CROSS_NO_MATCH) continue; - std::vector successor = frame.state; - successor[0] = candidate; - bool covered = false; - for (size_t i = 0; i < blockers.size(); ++i) { - const CrossRangeState match - = crossRangeStep(*blockers[i], frame.state[i + 1], frame.bit, value); - successor[i + 1] = match; - if (match != CROSS_NO_MATCH && freeBelow[i][match] >= frame.bit) { - covered = true; - break; - } - } - if (covered) continue; - if (frame.bit == 0) return CrossMatchResult::MATCH; - const int bit = frame.bit - 1; - if (failed.find({bit, successor}) == failed.end()) { - stack.push_back({bit, std::move(successor), 0}); - } - } - return CrossMatchResult::NO_MATCH; + // Resolve a bin or intersect value to coverpoint values (IEEE 1800-2023 19.5.7), in the + // width 'range' was created with. False if the value is not a constant integral or real. + static bool resolveValue(AstNode* nodep, AstNodeExpr* exprp, bool binValue, bool wildcard, + CrossValueRange& range) { + const CrossValueRange domain + = crossValueDomain(nodep, exprp->width(), exprp->isSigned(), range.lo.width()); + return crossValueRange(nodep, exprp, binValue, wildcard, domain, range); } - static CrossMatchResult crossRangesIntersect(const CrossValueRange& bin, - const CrossValueRange& filter, - const std::vector& excluded, - bool excludeValues) { - if (filter.lo.isFourState() || bin.lo.isFourState()) { - if (!bin.singleton || !filter.singleton || !bin.lo.isCaseEq(filter.lo)) { - return CrossMatchResult::NO_MATCH; - } - return (!excludeValues - || std::none_of(excluded.begin(), excluded.end(), - [&](const CrossValueRange& range) { - return !range.wildcard && range.singleton - && bin.lo.isCaseEq(range.lo); - })) - ? CrossMatchResult::MATCH - : CrossMatchResult::NO_MATCH; - } + static bool crossRangesIntersect(const CrossValueRange& bin, const CrossValueRange& filter) { + // Values with x or z bits do not participate, even in an identical filter. + if (bin.lo.isFourState() || filter.lo.isFourState()) return false; CrossValueRange match = bin; intersectCrossRange(match, filter); - if (crossValueLess(match.hi, match.lo)) return CrossMatchResult::NO_MATCH; - if (!excludeValues || excluded.empty()) { - return !bin.wildcard || crossWildcardIntersects(match) ? CrossMatchResult::MATCH - : CrossMatchResult::NO_MATCH; - } - return crossOutsideExcluded(match, excluded); + return !crossValueLess(match.hi, match.lo) + && (!bin.wildcard || crossWildcardIntersects(match)); } static void unsupportedCrossRange(AstCoverBinsof* selectp, bool& valid) { @@ -1890,7 +1937,6 @@ class FunctionalCoverageVisitor final : public VNVisitor { AstNodeExpr* exprp, const AstCoverBin* binp, const CrossValueRange& domain, const std::vector& filters, - const std::vector& excluded, bool& valid) { CrossValueRange range{valuep, domain.lo.width()}; if (!crossValueRange(valuep, exprp, true, binp->isWildcard(), domain, range)) { @@ -1898,22 +1944,17 @@ class FunctionalCoverageVisitor final : public VNVisitor { return false; } for (const CrossValueRange& filter : filters) { - // State exclusions do not remove values from transition sequences. - const CrossMatchResult result - = crossRangesIntersect(range, filter, excluded, !binp->transp()); - if (result == CrossMatchResult::WORK_LIMIT) { - selectp->v3warn(COVERIGN, "Unsupported: 'intersect' exclusion matching exceeds " - "the selection work limit."); - valid = false; - return false; - } - if (result == CrossMatchResult::MATCH) return true; + if (crossRangesIntersect(range, filter)) return true; } return false; } std::vector selectCoverpointBins(AstCoverBinsof* selectp, const CoverpointBins& bins, uint32_t first, uint32_t count, bool& valid) { + if (selectp->rangesp() && !bins.exprp->dtypep()->skipRefp()->isIntegralOrPacked()) { + unsupportedCrossRange(selectp, valid); + return {}; + } std::vector selected(bins.total, false); std::vector> values; int width = bins.exprp->width(); @@ -1921,11 +1962,6 @@ class FunctionalCoverageVisitor final : public VNVisitor { width = std::max(width, crossRangeWidth(rangep)); } if (selectp->rangesp()) { - for (AstCoverBin* const binp : bins.excluded) { - for (AstNode* rangep = binp->rangesp(); rangep; rangep = rangep->nextp()) { - width = std::max(width, crossRangeWidth(rangep)); - } - } values.reserve(count); for (uint32_t i = first; i < first + count; ++i) { values.push_back(crossBinValues(bins.values[i])); @@ -1947,29 +1983,14 @@ class FunctionalCoverageVisitor final : public VNVisitor { } filters.push_back(std::move(filter)); } - std::vector excluded; - if (selectp->rangesp()) { - for (AstCoverBin* const binp : bins.excluded) { - for (AstNode* rangep = binp->rangesp(); rangep; rangep = rangep->nextp()) { - CrossValueRange range{rangep, width}; - if (!crossValueRange(rangep, bins.exprp, true, binp->isWildcard(), domain, - range)) { - unsupportedCrossRange(selectp, valid); - return {}; - } - excluded.push_back(std::move(range)); - } - } - } for (uint32_t i = first; i < first + count; ++i) { if (!selectp->rangesp()) { selected[i] = true; continue; } for (AstNode* const valuep : values[i - first]) { - selected[i] - = crossValueMatchesFilters(selectp, valuep, bins.exprp, bins.values[i].binp, - domain, filters, excluded, valid); + selected[i] = crossValueMatchesFilters( + selectp, valuep, bins.exprp, bins.values[i].binp, domain, filters, valid); if (!valid) return {}; if (selected[i]) break; } @@ -1980,6 +2001,192 @@ class FunctionalCoverageVisitor final : public VNVisitor { return selected; } + // Constructor-time value metadata of one coverpoint, as C++ list entries + struct ValueLists final { + std::vector m_ranges; // Bin, then low and high words + std::vector m_patterns; // Bin, then value, mask, low, and high words + std::vector m_transitions; // Transition bin + }; + + // Append a value's words, in the coverpoint's width, to a C++ list entry. + static void appendWords(std::string& text, const V3Number& value, const AstNodeExpr* exprp) { + V3Number narrowed{&value, exprp->width()}; + narrowed.opAssign(value); + for (int word = 0; word < exprp->widthWords(); ++word) { + text += ", " + cvtToStr(narrowed.edataWord(word)) + "U"; + } + } + + void collectValueMetadata(ValueLists& lists, AstNodeExpr* exprp, AstCoverBin* binp, + uint32_t index, AstNodeExpr* valuep) { + const std::string bin = cvtToStr(index) + "U"; + if (binp->transp()) lists.m_transitions.push_back(bin); + for (AstNode* const sourcep : crossBinValues({binp, valuep})) { + CrossValueRange range{sourcep, resolveWidth(sourcep, exprp)}; + if (!resolveValue(sourcep, exprp, true, binp->isWildcard(), range)) { + // Sampling already resolved every state bin value, so only transitions remain. + sourcep->v3warn(E_UNSUPPORTED, "Unsupported: non-integral value in a transition " + "bin of a coverpoint with exclusions."); + continue; + } + if (crossRangeEmpty(range)) continue; + std::string entry = bin; + if (range.wildcard) { + V3Number value{sourcep, exprp->width()}; + V3Number mask{sourcep, exprp->width()}; + mask.opBitsNonXZ(range.pattern); + value.opBitsOne(range.pattern); + appendWords(entry, value, exprp); + appendWords(entry, mask, exprp); + } + appendWords(entry, range.lo, exprp); + appendWords(entry, range.hi, exprp); + (range.wildcard ? lists.m_patterns : lists.m_ranges).push_back(entry); + } + } + + // Emit one batched metadata list, bounding the size of each call's temporary list. + void emitValueList(FileLine* fl, AstVar* cpVarp, VCMethod method, + const std::vector& entries) { + for (size_t first = 0; first < entries.size(); first += VALUE_LIST_ENTRIES) { + const size_t end = std::min(entries.size(), first + VALUE_LIST_ENTRIES); + std::string text = "{" + entries[first]; + for (size_t i = first + 1; i < end; ++i) text += ", " + entries[i]; + m_constructorp->addStmtsp( + itemCall(fl, cpVarp, method, {ctext(fl, text + "}")})->makeStmt()); + } + } + + static bool checkCrossRef(const AstCoverCrossRef* refp, const AstCoverCross* crossp) { + if (refp->name() == crossp->name()) return true; + refp->v3error("Cross selection " + << refp->prettyNameQ() << " may only name its enclosing cross " + << crossp->prettyNameQ() << " (IEEE 1800-2023 19.6.1.2)."); + return false; + } + + static bool checkCrossBinName(const AstCoverCrossBin* binp, std::set& names) { + if (names.emplace(binp->name()).second) return true; + binp->v3error("Duplicate cross bin " << binp->prettyNameQ() + << " (IEEE 1800-2023 19.6.1)."); + return false; + } + + CrossBinsofTarget + resolveBinsofTarget(const AstCoverBinsof* selectp, const AstCoverCross* crossp, + const std::vector& cpVars, + const std::map& dimensions) const { + const auto dim = dimensions.find(selectp->pointp()->name()); + if (dim == dimensions.end()) { + selectp->v3error("binsof coverpoint " + << selectp->pointp()->prettyNameQ() << " is not an item of cross " + << crossp->prettyNameQ() << " (IEEE 1800-2023 19.6.1)."); + return {}; + } + const CoverpointBins& bins = m_cpBins.at(cpVars[dim->second]); + CrossBinsofTarget target{&bins, dim->second, 0, bins.total}; + if (!selectp->name().empty()) { + const auto bin = bins.spans.find(selectp->name()); + if (bin == bins.spans.end()) { + selectp->v3error("Cannot find bin " << selectp->prettyNameQ() << " in coverpoint " + << selectp->pointp()->prettyNameQ() + << " (IEEE 1800-2023 19.6.1)."); + return {}; + } + target.m_first = bin->second.first; + target.m_count = bin->second.count; + target.m_declaredFirst = bin->second.declared; + target.m_declaredEnd = bin->second.declared + bin->second.count; + } + return target; + } + + bool generateRuntimeSelection(AstNode* nodep, AstCoverCross* crossp, AstVar* cxp, + const std::vector& cpVars, + const std::map& dimensions) { + FileLine* const fl = nodep->fileline(); + if (const AstCoverCrossRef* const refp = VN_CAST(nodep, CoverCrossRef)) { + if (!checkCrossRef(refp, crossp)) return false; + m_constructorp->addStmtsp( + itemCall(fl, cxp, VCMethod::COVERGROUP_SELECT_ALL)->makeStmt()); + return true; + } + if (const AstCoverCrossSelect* const opp = VN_CAST(nodep, CoverCrossSelect)) { + if (!generateRuntimeSelection(opp->lhsp(), crossp, cxp, cpVars, dimensions) + || !generateRuntimeSelection(opp->rhsp(), crossp, cxp, cpVars, dimensions)) { + return false; + } + m_constructorp->addStmtsp(itemCall(fl, cxp, + opp->isOr() ? VCMethod::COVERGROUP_SELECT_OR + : VCMethod::COVERGROUP_SELECT_AND) + ->makeStmt()); + return true; + } + AstCoverBinsof* const selectp = VN_AS(nodep, CoverBinsof); + const CrossBinsofTarget target = resolveBinsofTarget(selectp, crossp, cpVars, dimensions); + if (!target.m_binsp) return false; + const CoverpointBins& bins = *target.m_binsp; + if (selectp->rangesp() && !bins.exprp->dtypep()->skipRefp()->isIntegralOrPacked()) { + bool valid = true; + unsupportedCrossRange(selectp, valid); + return false; + } + m_constructorp->addStmtsp( + itemCall(fl, cxp, VCMethod::COVERGROUP_SELECT_DIM, + {cnum(fl, target.m_dimension), cnum(fl, target.m_declaredFirst), + cnum(fl, target.m_declaredEnd), cnum(fl, selectp->isNegated()), + cnum(fl, selectp->rangesp() != nullptr)}) + ->makeStmt()); + for (AstNode* rangep = selectp->rangesp(); rangep; rangep = rangep->nextp()) { + CrossValueRange range{rangep, resolveWidth(rangep, bins.exprp)}; + if (!resolveValue(rangep, bins.exprp, false, false, range)) { + bool valid = true; + unsupportedCrossRange(selectp, valid); + return false; + } + if (crossRangeEmpty(range)) continue; + m_constructorp->addStmtsp(itemCall(fl, cxp, + bins.exprp->isWide() + ? VCMethod::COVERGROUP_SELECT_RANGE_W + : VCMethod::COVERGROUP_SELECT_RANGE, + {newValueConst(fl, range.lo, bins.exprp), + newValueConst(fl, range.hi, bins.exprp)}) + ->makeStmt()); + } + m_constructorp->addStmtsp( + itemCall(fl, cxp, VCMethod::COVERGROUP_SELECT_DIM_END)->makeStmt()); + return true; + } + + std::vector + generateRuntimeCrossBins(AstCoverCross* crossp, AstVar* cxp, + const std::vector& cpVars, + const std::map& dimensions) { + std::vector bins; + std::set names; + for (AstNode* itemp = crossp->binsp(); itemp; itemp = itemp->nextp()) { + AstCoverCrossBin* const binp = VN_AS(itemp, CoverCrossBin); + if (!checkCrossBinName(binp, names)) continue; + if (!generateRuntimeSelection(binp->selectp(), crossp, cxp, cpVars, dimensions)) { + continue; + } + FileLine* const fl = binp->fileline(); + const bool protect = v3Global.opt.protectIds(); + m_constructorp->addStmtsp( + itemCall(fl, cxp, VCMethod::COVERGROUP_SELECT_BIN, + {ctext(fl, binp->binsType().binSetEnum()), + ctext(fl, quoted(VIdProtect::protectWordsIf(binp->name(), protect))), + ctext(fl, quoted(VIdProtect::protectIf(fl->filename(), protect))), + cnum(fl, fl->lineno()), cnum(fl, fl->firstColumn()), + cnum(fl, static_cast(bins.size()))}) + ->makeStmt()); + bins.push_back(binp); + } + m_constructorp->addStmtsp( + itemCall(crossp->fileline(), cxp, VCMethod::COVERGROUP_FINALIZE_BINS)->makeStmt()); + return bins; + } + static void setCrossSelectionRange(CrossSelection& selection, uint64_t first, uint64_t end) { while (first < end) { const unsigned bit = first % 64; @@ -1992,10 +2199,7 @@ class FunctionalCoverageVisitor final : public VNVisitor { CrossSelection crossSelection(AstNode* nodep, CrossSelectionContext& ctx) { if (const AstCoverCrossRef* const refp = VN_CAST(nodep, CoverCrossRef)) { - if (refp->name() != ctx.crossp->name()) { - refp->v3error("Cross selection " - << refp->prettyNameQ() << " may only name its enclosing cross " - << ctx.crossp->prettyNameQ() << " (IEEE 1800-2023 19.6.1.2)."); + if (!checkCrossRef(refp, ctx.crossp)) { ctx.valid = false; return {}; } @@ -2014,36 +2218,19 @@ class FunctionalCoverageVisitor final : public VNVisitor { return lhs; } AstCoverBinsof* const selectp = VN_AS(nodep, CoverBinsof); - const auto dimIt = ctx.dimensions.find(selectp->pointp()->name()); - if (dimIt == ctx.dimensions.end()) { - selectp->v3error("binsof coverpoint " - << selectp->pointp()->prettyNameQ() << " is not an item of cross " - << ctx.crossp->prettyNameQ() << " (IEEE 1800-2023 19.6.1)."); + const CrossBinsofTarget target + = resolveBinsofTarget(selectp, ctx.crossp, ctx.cpVars, ctx.dimensions); + if (!target.m_binsp) { ctx.valid = false; return {}; } - const uint32_t dim = dimIt->second; - const CoverpointBins& bins = m_cpBins.at(ctx.cpVars[dim]); - uint32_t first = 0; - uint32_t count = bins.total; - if (!selectp->name().empty()) { - const auto binIt = bins.spans.find(selectp->name()); - if (binIt == bins.spans.end()) { - selectp->v3error("Cannot find bin " << selectp->prettyNameQ() << " in coverpoint " - << selectp->pointp()->prettyNameQ() - << " (IEEE 1800-2023 19.6.1)."); - ctx.valid = false; - return {}; - } - first = binIt->second.first; - count = binIt->second.second; - } + const CoverpointBins& bins = *target.m_binsp; const std::vector selected - = selectCoverpointBins(selectp, bins, first, count, ctx.valid); + = selectCoverpointBins(selectp, bins, target.m_first, target.m_count, ctx.valid); if (!ctx.valid) return {}; CrossSelection result(VL_BITWORD_Q(static_cast(ctx.tuples) + VL_QUADSIZE - 1), 0); - const uint64_t stride = ctx.strides[dim]; + const uint64_t stride = ctx.strides[target.m_dimension]; const uint64_t period = stride * bins.total; for (uint64_t base = 0; base < ctx.tuples; base += period) { for (uint32_t i = 0; i < bins.total;) { @@ -2059,37 +2246,43 @@ class FunctionalCoverageVisitor final : public VNVisitor { return result; } + // Size the Cartesian product of the declared Normal bins, with each dimension's flat-index + // stride. Live runtime bins only shrink it. Warns and returns false if it is too large. + bool crossShape(AstCoverCross* crossp, const std::vector& cpVars, + std::vector& strides, uint32_t& tuples) const { + uint64_t product = std::any_of(cpVars.begin(), cpVars.end(), + [this](AstVar* varp) { return !m_cpBins.at(varp).total; }) + ? 0 + : 1; + strides.resize(cpVars.size()); + for (size_t d = cpVars.size(); d > 0; --d) { + strides[d - 1] = static_cast(product); + product *= m_cpBins.at(cpVars[d - 1]).total; + if (product > UINT32_MAX) { + crossp->v3warn(COVERIGN, + "Unsupported: cross coverage with more than 2^32-1 tuples."); + return false; + } + } + tuples = static_cast(product); + return true; + } + CrossLayout resolveCrossLayout(AstCoverCross* crossp, const std::vector& cpVars, const std::map& dimensions) { CrossLayout layout; CrossSelectionContext ctx{crossp, cpVars, dimensions, 0, {}}; - uint64_t tuples = std::any_of(cpVars.begin(), cpVars.end(), - [this](AstVar* varp) { return !m_cpBins.at(varp).total; }) - ? 0 - : 1; - ctx.strides.resize(cpVars.size()); - for (size_t d = cpVars.size(); d > 0; --d) { - ctx.strides[d - 1] = tuples; - tuples *= m_cpBins.at(cpVars[d - 1]).total; - if (tuples > UINT32_MAX) { - crossp->v3warn(COVERIGN, - "Unsupported: cross coverage with more than 2^32-1 tuples."); - layout.valid = false; - return layout; - } + if (!crossShape(crossp, cpVars, ctx.strides, ctx.tuples)) { + layout.valid = false; + return layout; } - ctx.tuples = tuples; - layout.tuples = tuples; + layout.tuples = ctx.tuples; CrossSelection occupied; CrossSelection excluded; std::set names; for (AstNode* itemp = crossp->binsp(); itemp; itemp = itemp->nextp()) { AstCoverCrossBin* const binp = VN_AS(itemp, CoverCrossBin); - if (!names.emplace(binp->name()).second) { - binp->v3error("Duplicate cross bin " << binp->prettyNameQ() - << " (IEEE 1800-2023 19.6.1)."); - continue; - } + if (!checkCrossBinName(binp, names)) continue; ctx.valid = true; CrossSelection selection = crossSelection(binp->selectp(), ctx); if (!ctx.valid || std::all_of(selection.begin(), selection.end(), [](uint64_t word) { @@ -2134,13 +2327,15 @@ class FunctionalCoverageVisitor final : public VNVisitor { return layout; } - AstCoverCrossDType* crossDType(FileLine* fl, uint32_t dimensions, const CrossLayout& layout) { + AstCoverCrossDType* crossDType(FileLine* fl, uint32_t dimensions, const CrossLayout& layout, + bool dynamic = false) { const uint32_t bins = static_cast(layout.bins.size()); - const CrossShape shape{dimensions, layout.tuples, bins, layout.autoBins, layout.binWords}; + const CrossShape shape{dimensions, layout.tuples, bins, + layout.autoBins, layout.binWords, dynamic}; AstCoverCrossDType*& typep = m_cxDTypes[shape]; if (!typep) { - typep = new AstCoverCrossDType{fl, dimensions, layout.tuples, - bins, layout.autoBins, layout.binWords}; + typep = new AstCoverCrossDType{ + fl, dimensions, layout.tuples, bins, layout.autoBins, layout.binWords, dynamic}; v3Global.rootp()->typeTablep()->addTypesp(typep); } return typep; @@ -2183,6 +2378,7 @@ class FunctionalCoverageVisitor final : public VNVisitor { // (their hit lists drive the cross). Each explicit bin adds one configuration call. void generateCross(AstCoverCross* crossp) { FileLine* const fl = crossp->fileline(); + const bool dynamic = m_runtimeCrosses.count(crossp); UINFO(4, " Generating VlCoverCross member: " << crossp->name()); if (AstNodeExpr* const iffp = crossp->iffp()) { @@ -2205,14 +2401,25 @@ class FunctionalCoverageVisitor final : public VNVisitor { itemp = nextp; } const int dims = static_cast(cpVars.size()); - const CrossLayout layout = resolveCrossLayout(crossp, cpVars, dimensions); + CrossLayout layout; + if (dynamic) { + // The runtime sizes the layout from live bins; only check the declared bound here. + std::vector strides; + uint32_t tuples = 0; + layout.valid = crossShape(crossp, cpVars, strides, tuples); + } else { + layout = resolveCrossLayout(crossp, cpVars, dimensions); + } if (!layout.valid) return; - AstVar* const cxVarp = new AstVar{fl, VVarType::MEMBER, "__Vcx_" + crossp->name(), - crossDType(fl, static_cast(dims), layout)}; + AstVar* const cxVarp + = new AstVar{fl, VVarType::MEMBER, "__Vcx_" + crossp->name(), + crossDType(fl, static_cast(dims), layout, dynamic)}; m_covergroupp->addMembersp(cxVarp); m_crossVars.push_back(cxVarp); - m_constructorp->addStmtsp(makeItemCreate(fl, cxVarp, VCMethod::COVERGROUP_ADD_CROSS)); + m_constructorp->addStmtsp(makeItemCreate(fl, cxVarp, + dynamic ? VCMethod::COVERGROUP_ADD_CROSS_DYN + : VCMethod::COVERGROUP_ADD_CROSS)); // Constructor: init (after the coverpoints, which generate earlier) then registration. // Obfuscate the hierarchy/filename/page under --protect-ids as for coverpoints above. @@ -2227,7 +2434,9 @@ class FunctionalCoverageVisitor final : public VNVisitor { ctext(fl, quoted(VIdProtect::protectIf(fl->filename(), prot))), cnum(fl, static_cast(fl->lineno())), cnum(fl, static_cast(fl->firstColumn()))}))); - const std::vector bins = generateCrossBins(crossp, cxVarp, layout); + const std::vector bins + = dynamic ? generateRuntimeCrossBins(crossp, cxVarp, cpVars, dimensions) + : generateCrossBins(crossp, cxVarp, layout); if (v3Global.opt.coverage()) { const std::string page = VIdProtect::protectIf("v_covergroup/" + m_covergroupp->name(), prot); @@ -2303,8 +2512,10 @@ class FunctionalCoverageVisitor final : public VNVisitor { AstNode* const rangep = binp->rangesp(); if (!rangep) return nullptr; - // Check if this is a wildcard bin - const bool isWildcard = binp->isWildcard(); + // Integral values resolve to the coverpoint's type, as its runtime metadata does + const bool integral = exprp->dtypep()->skipRefp()->isIntegralOrPacked(); + // No value form of a wildcard bin is allowed on a real coverpoint (IEEE 1800-2023 19.5.4) + if (binp->isWildcard() && !integral) return wildcardTypeError(binp, exprp); // Build condition by OR-ing all ranges together AstNodeExpr* fullCondp = nullptr; @@ -2336,6 +2547,8 @@ class FunctionalCoverageVisitor final : public VNVisitor { "range bounds must be two-state constants"); if (fullCondp) VL_DO_DANGLING(pushDeletep(fullCondp), fullCondp); return nullptr; + } else if (integral) { + rangeCondp = buildValueCondition(binp, exprp, irp); } else { rangeCondp = makeOpenRangeCondition(irp->fileline(), exprp, boundp, /*isLowerBound=*/hiUnbounded); @@ -2350,27 +2563,13 @@ class FunctionalCoverageVisitor final : public VNVisitor { "range bounds must be two-state constants"); if (fullCondp) VL_DO_DANGLING(pushDeletep(fullCondp), fullCondp); return nullptr; - } else if (minConstp->toUQuad() == maxConstp->toUQuad()) { - // Single value - if (isWildcard) { - rangeCondp = buildWildcardCondition(binp, exprp, minConstp); - } else { - rangeCondp = new AstEq{binp->fileline(), exprp->cloneTree(false), - minExprp->cloneTree(false)}; - } + } else if (integral) { + rangeCondp = buildValueCondition(binp, exprp, irp); } else { rangeCondp = makeRangeCondition(irp->fileline(), exprp, minExprp, maxExprp); } } else if (AstConst* constp = VN_CAST(currRangep, Const)) { - if (isWildcard) { - rangeCondp = buildWildcardCondition(binp, exprp, constp); - } else { - // TODO: A 4-state bin value (e.g. bins b = {2'b0x}) must match with === - // (AstEqCase) per IEEE 1800-2023 19.5.4. == is equivalent under 2-state sim - // (x/z collapse to 0); switch to AstEqCase when 4-state sim support lands. - rangeCondp = new AstEq{binp->fileline(), exprp->cloneTree(false), - constp->cloneTree(false)}; - } + rangeCondp = buildValueCondition(binp, exprp, constp); } else { currRangep->v3error("Non-constant expression in bin range; values must be " "constants (IEEE 1800-2023 19.5)"); @@ -2386,37 +2585,54 @@ class FunctionalCoverageVisitor final : public VNVisitor { return fullCondp; } - // Build a wildcard condition: (expr & mask) == (value & mask) - // where mask has 1s for defined bits and 0s for wildcard bits - // Non-owning: exprp is cloned internally; caller retains ownership. - AstNodeExpr* buildWildcardCondition(AstCoverBin* binp, AstNodeExpr* exprp, AstConst* constp) { - FileLine* const fl = binp->fileline(); + // Wildcard bits have no meaning for a non-integral coverpoint. + static AstNodeExpr* wildcardTypeError(AstCoverBin* binp, AstNodeExpr* exprp) { + const AstNodeDType* const dtypep = exprp->dtypep()->skipRefp(); + exprp->v3error("Cannot use a wildcard bin on a coverpoint of type " + << dtypep->prettyDTypeNameQ() << " (IEEE 1800-2023 19.5.4).\n" + << exprp->warnContextPrimary() << '\n' + << binp->warnOther() << "... Location of wildcard bin\n" + << binp->warnContextSecondary()); + return new AstConst{binp->fileline(), AstConst::BitFalse{}}; + } - // Extract mask from constant (bits that are not X/Z) - V3Number mask{constp, constp->width()}; - V3Number value{constp, constp->width()}; - - for (int bit = 0; bit < constp->width(); ++bit) { - if (constp->num().bitIs0(bit) || constp->num().bitIs1(bit)) { - mask.setBit(bit, 1); - value.setBit(bit, constp->num().bitIs1(bit) ? 1 : 0); - } else { - mask.setBit(bit, 0); - value.setBit(bit, 0); - } + // Match one bin value, range, or wildcard pattern. Integral coverpoints first resolve the + // value to their type (IEEE 1800-2023 19.5.7). Non-owning: clones what it uses. + AstNodeExpr* buildValueCondition(AstCoverBin* binp, AstNodeExpr* exprp, AstNode* valuep) { + FileLine* const fl = valuep->fileline(); + if (!exprp->dtypep()->skipRefp()->isIntegralOrPacked()) { + return new AstEq{fl, exprp->cloneTree(false), + VN_AS(valuep, NodeExpr)->cloneTree(false)}; } - - // Generate: (expr & mask) == (value & mask) - AstConst* const maskConstp = new AstConst{fl, mask}; - AstConst* const valueConstp = new AstConst{fl, value}; - + CrossValueRange range{valuep, resolveWidth(valuep, exprp)}; + if (!resolveValue(valuep, exprp, true, binp->isWildcard(), range)) { + valuep->v3warn(E_UNSUPPORTED, + "Unsupported: non-integral value in a coverage bin of an " + "integral coverpoint."); + return new AstConst{fl, AstConst::BitFalse{}}; + } + if (crossRangeEmpty(range)) return new AstConst{fl, AstConst::BitFalse{}}; + AstConst* const lop = newValueConst(fl, range.lo, exprp); + AstConst* const hip = newValueConst(fl, range.hi, exprp); + AstNodeExpr* condp = nullptr; + if (lop->num().isCaseEq(hip->num())) { + condp = new AstEq{fl, exprp->cloneTree(false), lop}; + } else { + condp = makeRangeCondition(fl, exprp, lop, hip); + VL_DO_DANGLING(pushDeletep(lop), lop); + } + VL_DO_DANGLING(pushDeletep(hip), hip); + if (!range.wildcard) return condp; + // Match the pattern's value bits within the source-value bounds. + V3Number mask{valuep, exprp->width()}; + V3Number value{valuep, exprp->width()}; + mask.opBitsNonXZ(range.pattern); + value.opBitsOne(range.pattern); + AstConst* const maskConstp = newValueConst(fl, mask, exprp); + AstConst* const valueConstp = newValueConst(fl, value, exprp); AstNodeExpr* const exprMasked = new AstAnd{fl, exprp->cloneTree(false), maskConstp}; AstNodeExpr* const valueMasked = new AstAnd{fl, valueConstp, maskConstp->cloneTree(false)}; - - // TODO: masking the wildcard (don't-care) bits is correct, but the defined-bit - // comparison should use === (AstEqCase) per IEEE 1800-2023 19.5.4 once 4-state sim - // support lands; == is equivalent under 2-state sim (x/z collapse to 0). - return new AstEq{fl, exprMasked, valueMasked}; + return new AstLogAnd{fl, condp, new AstEq{fl, exprMasked, valueMasked}}; } void generateCoverageComputationCode() { @@ -3004,9 +3220,6 @@ public: ~FunctionalCoverageVisitor() override = default; }; -// C++14 requires definitions for constexpr members passed by reference. -constexpr size_t FunctionalCoverageVisitor::CROSS_MATCH_WORK_LIMIT; - //###################################################################### // Functional coverage class functions diff --git a/test_regress/t/t_covergroup_auto_bin_max.out b/test_regress/t/t_covergroup_auto_bin_max.out index 610b8bece..6bfc6a556 100644 --- a/test_regress/t/t_covergroup_auto_bin_max.out +++ b/test_regress/t/t_covergroup_auto_bin_max.out @@ -12,16 +12,18 @@ cg2.cp_data3.auto_2: 1 cg2.cp_data3.auto_3: 0 cg3.cp_data3.auto_0: 1 cg3.cp_data3.auto_1: 1 -cg4.cp.auto_0: 0 cg4.cp.auto_1: 1 cg4.cp.auto_2: 1 cg4.cp.auto_3: 1 cg4.cp.ign [ignore]: 0 -cg5.cp_data64.auto_0: 1 -cg5.cp_data64.auto_1: 1 -cg5.cp_data64.auto_2: 1 +cg5.cp_data64.auto_0: 2 +cg5.cp_data64.auto_1: 0 +cg5.cp_data64.auto_2: 0 cg5.cp_data64.auto_3: 1 cg6.cp_data3.auto_0: 1 cg6.cp_data3.auto_1: 0 cg6.cp_data3.auto_2: 0 cg6.cp_data3.auto_3: 1 +cg7.cp_sdata3.auto_0: 1 +cg7.cp_sdata3.auto_1: 1 +cg7.cp_sdata3.auto_2: 2 diff --git a/test_regress/t/t_covergroup_auto_bin_max.v b/test_regress/t/t_covergroup_auto_bin_max.v index f4631c2ee..33ad76229 100644 --- a/test_regress/t/t_covergroup_auto_bin_max.v +++ b/test_regress/t/t_covergroup_auto_bin_max.v @@ -16,6 +16,7 @@ module t; logic [2:0] data3; logic [3:0] data4; logic [63:0] data64; // 64-bit signal + logic signed [2:0] sdata3; // Test 1: auto_bin_max default (64) - creates 8 bins for 3-bit signal covergroup cg1; @@ -58,6 +59,13 @@ module t; cp_data3: coverpoint data3; endgroup + // Test 7: signed values are partitioned in value order, the last bin taking the remainder: + // [-4:-3],[-2:-1],[0:3] + covergroup cg7; + option.auto_bin_max = 3; + cp_sdata3: coverpoint sdata3; + endgroup + initial begin cg1 cg1_inst; cg2 cg2_inst; @@ -65,6 +73,7 @@ module t; cg4 cg4_inst; cg5 cg5_inst; cg6 cg6_inst; + cg7 cg7_inst; cg1_inst = new; cg2_inst = new; @@ -72,6 +81,7 @@ module t; cg4_inst = new; cg5_inst = new; cg6_inst = new; + cg7_inst = new; data3 = 0; cg1_inst.sample(); @@ -97,23 +107,25 @@ module t; // Sample valid (non-ignored) values for cg4 // cg4: auto_bin_max=4 creates 4 bins [0:3],[4:7],[8:11],[12:15]. - // ignore_bins ign={[0:3]} excludes [0:3] values; Verilator keeps all 4 bins in denominator. - // 3 of 4 bins hit -> 75% (the [0:3] bin is included in denominator but can never be hit) + // The empty [0:3] bin is excluded from the denominator (IEEE 1800-2023 19.11.1). data4 = 4; cg4_inst.sample(); // [4:7] bin data4 = 8; cg4_inst.sample(); // [8:11] bin data4 = 12; cg4_inst.sample(); // [12:15] bin - `checkr(cg4_inst.get_inst_coverage(), 75.0); + `checkr(cg4_inst.get_inst_coverage(), 100.0); - // Sample cg5: 64-bit coverpoint - SKIP: Verilator 64-bit bin boundary bug causes 100% at first sample + // Sample cg5: the full 64-bit domain is partitioned into four bins. data64 = 64'h0; cg5_inst.sample(); + `checkr(cg5_inst.get_inst_coverage(), 25.0); data64 = 64'h1111111111111111; cg5_inst.sample(); + `checkr(cg5_inst.get_inst_coverage(), 25.0); data64 = 64'hffffffffffffffff; cg5_inst.sample(); + `checkr(cg5_inst.get_inst_coverage(), 50.0); data3 = 0; cg6_inst.sample(); @@ -122,6 +134,17 @@ module t; cg6_inst.sample(); `checkr(cg6_inst.get_inst_coverage(), 50.0); // 2/4 bins hit: [0:1],[6:7] + sdata3 = -4; + cg7_inst.sample(); // [-4:-3] + sdata3 = 3; + cg7_inst.sample(); // [0:3] + `checkr(cg7_inst.get_inst_coverage(), 100.0 * 2.0 / 3.0); + sdata3 = 0; + cg7_inst.sample(); // [0:3] + sdata3 = -1; + cg7_inst.sample(); // [-2:-1] + `checkr(cg7_inst.get_inst_coverage(), 100.0); + $write("*-* All Finished *-*\n"); $finish; end diff --git a/test_regress/t/t_covergroup_auto_exclusions.out b/test_regress/t/t_covergroup_auto_exclusions.out new file mode 100644 index 000000000..809c86ff8 --- /dev/null +++ b/test_regress/t/t_covergroup_auto_exclusions.out @@ -0,0 +1,183 @@ +cg_cross.aa.auto_1: 4 +cg_cross.aa.auto_2: 5 +cg_cross.aa.high [illegal]: 0 +cg_cross.aa.low [ignore]: 0 +cg_cross.automatic_cross.auto_1_x_values[0] [cross]: 1 +cg_cross.automatic_cross.auto_1_x_values[2] [cross]: 1 +cg_cross.automatic_cross.auto_1_x_values[3] [cross]: 1 +cg_cross.automatic_cross.auto_2_x_values[0] [cross]: 1 +cg_cross.automatic_cross.auto_2_x_values[2] [cross]: 2 +cg_cross.automatic_cross.auto_2_x_values[3] [cross]: 1 +cg_cross.bb.ignored [ignore]: 2 +cg_cross.bb.values[0]: 2 +cg_cross.bb.values[2]: 3 +cg_cross.bb.values[3]: 2 +cg_cross.xx.all [cross]: 2 +cg_cross.xx.column [cross]: 1 +cg_cross.xx.forbidden [illegal,cross]: 0 +cg_cross.xx.grouped [cross]: 3 +cg_cross.xx.ignored [ignore,cross]: 1 +cg_cross.xx.row [cross]: 1 +cg_empty.aa.ignored [ignore]: 0 +cg_empty.bb.auto_0: 0 +cg_empty.bb.auto_1: 1 +cg_empty.bb.auto_2: 1 +cg_empty.bb.auto_3: 0 +cg_explicit.cp.all: 5 +cg_explicit.cp.forbidden [illegal]: 0 +cg_explicit.cp.ignored[0] [ignore]: 1 +cg_explicit.cp.ignored[1] [ignore]: 1 +cg_explicit.cp.mixed: 1 +cg_explicit.cp.values[1]: 1 +cg_explicit.cp.values[2]: 1 +cg_many_values.cp.ignored [ignore]: 0 +cg_many_values.cp.odd: 2 +cg_partition.cp.auto_1: 2 +cg_partition.cp.auto_2: 1 +cg_partition.cp.auto_3: 1 +cg_partition.cp.ignored [ignore]: 4 +cg_pattern_carry.cp_signed.ignored [ignore]: 1 +cg_pattern_carry.cp_signed.patterned: 4 +cg_pattern_carry.cp_unsigned.ignored [ignore]: 1 +cg_pattern_carry.cp_unsigned.patterned: 4 +cg_pattern_carry.other.auto_0: 8 +cg_pattern_carry.other.auto_1: 8 +cg_pattern_carry.signed_cross.carry [cross]: 4 +cg_pattern_carry.unsigned_cross.carry [cross]: 4 +cg_projection.negative.keep: 1 +cg_projection.negative.positive [ignore]: 1 +cg_projection.positive.keep: 1 +cg_projection.positive.negative [ignore]: 1 +cg_real_point.cp.low: 1 +cg_real_point.cp.middle: 1 +cg_real_point.cp.three: 1 +cg_real_values.cp.high: 1 +cg_real_values.cp.ignored [ignore]: 1 +cg_real_values.cp.low: 1 +cg_real_values.cp.middle: 2 +cg_real_values.cp.two: 1 +cg_real_values.plain.middle: 2 +cg_real_values.plain.two: 1 +cg_resolution.cp.clipped: 4 +cg_resolution.cp.kept: 4 +cg_resolution.cp.rest [default]: 4 +cg_resolution.cp.values[0]: 2 +cg_resolution.cx.clipped_x_auto_0 [cross]: 2 +cg_resolution.cx.clipped_x_auto_1 [cross]: 2 +cg_resolution.cx.kept_values [cross]: 4 +cg_resolution.cx.values[0]_x_auto_0 [cross]: 1 +cg_resolution.cx.values[0]_x_auto_1 [cross]: 1 +cg_resolution.other.auto_0: 7 +cg_resolution.other.auto_1: 7 +cg_resolution_excl.cp.clipped: 4 +cg_resolution_excl.cp.kept: 4 +cg_resolution_excl.cp.rest [default]: 4 +cg_resolution_excl.cp.unused [ignore]: 0 +cg_resolution_excl.cp.values[0]: 2 +cg_resolution_excl.cx.clipped_x_auto_0 [cross]: 2 +cg_resolution_excl.cx.clipped_x_auto_1 [cross]: 2 +cg_resolution_excl.cx.kept_values [cross]: 4 +cg_resolution_excl.cx.values[0]_x_auto_0 [cross]: 1 +cg_resolution_excl.cx.values[0]_x_auto_1 [cross]: 1 +cg_resolution_excl.other.auto_0: 7 +cg_resolution_excl.other.auto_1: 7 +cg_signed.cp.auto_1: 1 +cg_signed.cp.auto_2: 1 +cg_signed.cp.ignored [ignore]: 1 +cg_signed_wide.cp.auto_1: 2 +cg_signed_wide.cp.auto_2: 1 +cg_signed_wide.cp.auto_3: 1 +cg_signed_wide.cp.ignored [ignore]: 1 +cg_signed_wide.cx.auto_2_x_auto_0 [cross]: 1 +cg_signed_wide.cx.auto_2_x_auto_1 [cross]: 0 +cg_signed_wide.cx.auto_3_x_auto_0 [cross]: 0 +cg_signed_wide.cx.negative [cross]: 2 +cg_signed_wide.cx.positive [cross]: 1 +cg_signed_wide.other.auto_0: 3 +cg_signed_wide.other.auto_1: 2 +cg_source_width.cp.ignored [ignore]: 0 +cg_source_width.cp.keep: 2 +cg_unsigned_pattern.cp.all_values: 1 +cg_unsigned_pattern.cp.ignored [ignore]: 0 +cg_unsigned_pattern.cp.negative: 1 +cg_unsigned_pattern.cp.negative_pair: 1 +cg_wide.cp.auto_1: 1 +cg_wide.cp.auto_2: 1 +cg_wide.cp.auto_3: 1 +cg_wide.cp.ignored [ignore]: 1 +cg_wide_cross.aa.all: 129 +cg_wide_cross.aa.high[0]: 2 +cg_wide_cross.aa.high[10]: 2 +cg_wide_cross.aa.high[11]: 2 +cg_wide_cross.aa.high[12]: 2 +cg_wide_cross.aa.high[13]: 2 +cg_wide_cross.aa.high[14]: 2 +cg_wide_cross.aa.high[15]: 2 +cg_wide_cross.aa.high[16]: 2 +cg_wide_cross.aa.high[17]: 2 +cg_wide_cross.aa.high[18]: 2 +cg_wide_cross.aa.high[19]: 2 +cg_wide_cross.aa.high[1]: 2 +cg_wide_cross.aa.high[20]: 2 +cg_wide_cross.aa.high[21]: 2 +cg_wide_cross.aa.high[22]: 2 +cg_wide_cross.aa.high[23]: 2 +cg_wide_cross.aa.high[24]: 2 +cg_wide_cross.aa.high[25]: 2 +cg_wide_cross.aa.high[26]: 2 +cg_wide_cross.aa.high[27]: 2 +cg_wide_cross.aa.high[28]: 2 +cg_wide_cross.aa.high[29]: 2 +cg_wide_cross.aa.high[2]: 2 +cg_wide_cross.aa.high[30]: 2 +cg_wide_cross.aa.high[31]: 2 +cg_wide_cross.aa.high[3]: 2 +cg_wide_cross.aa.high[4]: 2 +cg_wide_cross.aa.high[5]: 2 +cg_wide_cross.aa.high[6]: 2 +cg_wide_cross.aa.high[7]: 2 +cg_wide_cross.aa.high[8]: 2 +cg_wide_cross.aa.high[9]: 2 +cg_wide_cross.aa.ignored [ignore]: 2 +cg_wide_cross.aa.low[10]: 2 +cg_wide_cross.aa.low[11]: 2 +cg_wide_cross.aa.low[12]: 2 +cg_wide_cross.aa.low[13]: 2 +cg_wide_cross.aa.low[14]: 2 +cg_wide_cross.aa.low[15]: 2 +cg_wide_cross.aa.low[16]: 2 +cg_wide_cross.aa.low[17]: 2 +cg_wide_cross.aa.low[18]: 2 +cg_wide_cross.aa.low[19]: 2 +cg_wide_cross.aa.low[1]: 3 +cg_wide_cross.aa.low[20]: 2 +cg_wide_cross.aa.low[21]: 2 +cg_wide_cross.aa.low[22]: 2 +cg_wide_cross.aa.low[23]: 2 +cg_wide_cross.aa.low[24]: 2 +cg_wide_cross.aa.low[25]: 2 +cg_wide_cross.aa.low[26]: 2 +cg_wide_cross.aa.low[27]: 2 +cg_wide_cross.aa.low[28]: 2 +cg_wide_cross.aa.low[29]: 2 +cg_wide_cross.aa.low[2]: 2 +cg_wide_cross.aa.low[30]: 2 +cg_wide_cross.aa.low[31]: 2 +cg_wide_cross.aa.low[32]: 2 +cg_wide_cross.aa.low[3]: 2 +cg_wide_cross.aa.low[4]: 2 +cg_wide_cross.aa.low[5]: 2 +cg_wide_cross.aa.low[6]: 2 +cg_wide_cross.aa.low[7]: 2 +cg_wide_cross.aa.low[8]: 2 +cg_wide_cross.aa.low[9]: 2 +cg_wide_cross.aa.middle: 129 +cg_wide_cross.bb.auto_0: 66 +cg_wide_cross.bb.auto_1: 65 +cg_wide_cross.xx.disabled [cross]: 0 +cg_wide_cross.xx.enabled_bin [cross]: 128 +cg_wide_cross.xx.tail [cross]: 128 +cg_wild.cp.ignored [ignore]: 1 +cg_wild.cp.odd_values: 2 +cg_wild_empty.cp.even_values [ignore]: 2 +cg_wild_empty.cp.odd_values [ignore]: 2 diff --git a/test_regress/t/t_covergroup_auto_exclusions.py b/test_regress/t/t_covergroup_auto_exclusions.py new file mode 100755 index 000000000..0231c343a --- /dev/null +++ b/test_regress/t/t_covergroup_auto_exclusions.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap +import coverage_covergroup_common + +test.scenarios('vlt_all') + +coverage_covergroup_common.run(test, + verilator_flags2=['--timing', '--dump-tree', '--dump-tree-json'], + threads=(2 if test.vltmt else 1)) + +test.passes() diff --git a/test_regress/t/t_covergroup_auto_exclusions.v b/test_regress/t/t_covergroup_auto_exclusions.v new file mode 100644 index 000000000..bc199d8a6 --- /dev/null +++ b/test_regress/t/t_covergroup_auto_exclusions.v @@ -0,0 +1,384 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkr(gotv,expv) do if ((gotv) != (expv)) begin $write("%%Error: %s:%0d: got=%f exp=%f\n", `__FILE__, `__LINE__, (gotv), (expv)); `stop; end while (0); +// verilog_format: on + +module t ( + input clk +); + int cyc = 0; + + // IEEE 1800-2023 19.11.1: retain auto_1={2,3}, auto_2={4}, auto_3={7}. + covergroup cg_partition with function sample (bit [2:0] value); + option.auto_bin_max = 4; + cp: coverpoint value { + ignore_bins ignored = {0, 1, 5, 6}; + } + endgroup + + covergroup cg_explicit with function sample (bit [2:0] value); + cp: coverpoint value { + bins all = {[0 : 7]}; + bins empty = {0, 1}; + bins mixed = {1, 3, 5}; + bins values[] = {0, 2, 4}; + wildcard bins outside_domain = {4'b1???}; + ignore_bins ignored[] = {0, 1}; + illegal_bins forbidden = {5} iff (0); + } + endgroup + + covergroup cg_cross with function sample (bit [3:0] a, bit [1:0] b, bit enabled); + option.auto_bin_max = 4; + aa: coverpoint a { + ignore_bins low = {[0 : 3]}; + illegal_bins high = {[12 : 15]} iff (0); + } + bb: coverpoint b { + bins values[] = {[0 : 3]}; + ignore_bins ignored = {1}; + } + automatic_cross: cross aa, bb; + xx: cross aa, bb{ + bins empty = binsof (aa) intersect {0} iff (0); + bins removed = binsof (aa) intersect {8} && binsof (bb) intersect {3} iff (0); + bins all = xx iff (enabled); + bins row = binsof (aa) intersect {4} iff (!enabled); + bins column = binsof (bb.values) intersect {2} iff (enabled); + bins grouped = (xx && binsof (aa) intersect {8}) || binsof (bb) intersect {2}; + ignore_bins ignored = binsof (aa) intersect {8} && binsof (bb) intersect {3} iff (enabled); + illegal_bins forbidden = binsof (aa) intersect {4} && binsof (bb) intersect {2} iff (0); + } + endgroup + + covergroup cg_wide_cross with function sample (bit [6:0] a, bit b, bit enabled); + aa: coverpoint a { + bins low[] = {[0 : 32]}; + bins middle = {[0 : 64]}; + bins high[] = {[33 : 64]}; + bins all = {[0 : 64]}; + ignore_bins ignored = {0}; + } + bb: coverpoint b; + xx: cross aa, bb{ + bins empty = binsof (aa.low) intersect {0} iff (0); + bins disabled = xx iff (0); + bins enabled_bin = xx iff (enabled); + bins tail = binsof (aa.all) iff (enabled); + } + endgroup + + covergroup cg_empty with function sample (bit [1:0] a, bit [1:0] b); + aa: coverpoint a { + ignore_bins ignored = {[0 : 3]} iff (0); + } + bb: coverpoint b; + xx: cross aa, bb{bins empty = xx iff (0);} + endgroup + + covergroup cg_signed with function sample (bit signed [6:0] value); + option.auto_bin_max = 3; + cp: coverpoint value { + ignore_bins ignored = {[-64 : -23]}; + } + endgroup + + covergroup cg_wide with function sample (bit [64:0] value); + option.auto_bin_max = 4; + cp: coverpoint value { + ignore_bins ignored = {[65'h0 : 65'h0_7fffffffffffffff]}; + } + endgroup + + covergroup cg_wild with function sample (bit [64:0] value); + cp: coverpoint value { + wildcard bins even_values = {65'bx0}; + wildcard bins odd_values = {65'bx1}; + wildcard ignore_bins ignored = {65'bx0}; + } + endgroup + + covergroup cg_wild_empty with function sample (bit [64:0] value); + option.auto_bin_max = 4; + cp: coverpoint value { + wildcard ignore_bins even_values = {65'bx0}; + wildcard ignore_bins odd_values = {65'bx1}; + } + endgroup + + covergroup cg_projection with function sample (bit signed [2:0] value); + negative: coverpoint value { + bins keep = {3'sb111}; + wildcard bins empty = {5'b01???}; + wildcard bins signed_empty = {5'sb01???}; + wildcard ignore_bins positive = {4'sb0???}; + } + positive: coverpoint value { + bins keep = {3'sb001}; + wildcard ignore_bins negative = {4'sb1???}; + } + endgroup + + covergroup cg_source_width with function sample (bit signed [6:0] value); + cp: coverpoint value { + wildcard bins keep = {2'sbx0}; + ignore_bins ignored = {1}; + } + endgroup + + covergroup cg_unsigned_pattern with function sample (bit signed [2:0] value); + cp: coverpoint value { + wildcard bins all_values = {4'b0???}; + wildcard bins negative_pair = {4'b011?}; + bins negative = {4'b0111}; + ignore_bins ignored = {0}; + } + endgroup + + covergroup cg_zero_limit with function sample (bit value); + option.auto_bin_max = 0; + cp: coverpoint value; + endgroup + + covergroup cg_signed_wide with function sample (bit signed [64:0] value, bit side); + option.auto_bin_max = 4; + cp: coverpoint value { + ignore_bins ignored = {[$ : 65'sh1_7fffffffffffffff]}; + } + other: coverpoint side; + cx: cross cp, other{ + bins negative = binsof (cp) intersect {65'sh1_ffffffffffffffff}; + bins positive = binsof (cp) intersect {65'sh0_8000000000000000} && binsof (other) intersect { + 1 + }; + } + endgroup + + covergroup cg_pattern_carry with function sample ( + bit [3:0] unsigned_value, bit signed [3:0] signed_value, bit side + ); + cp_unsigned: coverpoint unsigned_value { + wildcard bins patterned = {4'b?0?0}; + ignore_bins ignored = {15}; + } + cp_signed: coverpoint signed_value { + wildcard bins patterned = {4'sb?0?0}; + ignore_bins ignored = {4'shf}; + } + other: coverpoint side; + unsigned_cross: cross cp_unsigned, other{ + bins carry = binsof (cp_unsigned.patterned) intersect {[3 : 9]}; + bins empty = binsof (cp_unsigned.patterned) intersect {[11 : 15]}; + } + signed_cross: cross cp_signed, other{ + bins carry = binsof (cp_signed.patterned) intersect {[-5 : 1]}; + } + endgroup + + // Bin values resolve to the coverpoint's type (IEEE 1800-2023 19.5.7) and bins without + // values do not count (19.11.1), whether or not the coverpoint has exclusions. The two + // covergroups differ only by an ignore_bins that is never hit. + // verilog_format: off +`define RESOLUTION_BINS \ + bins outside = {[8 : 9]}; \ + bins unknown = {3'bx01}; \ + bins wide = {15}; \ + bins reversed = {[3 : 1]}; \ + bins kept = {[2 : 3]}; \ + bins clipped = {[6 : 10]}; \ + bins values[] = {4, 3'bx11}; \ + bins rest = default; +`define RESOLUTION_CROSS \ + other: coverpoint side; \ + cx: cross cp, other { \ + bins unknown_values = binsof (cp) intersect {3'bx01}; \ + bins kept_values = binsof (cp.kept); \ + } + // verilog_format: on + covergroup cg_resolution with function sample (logic [2:0] value, bit side); + cp: coverpoint value {`RESOLUTION_BINS} + `RESOLUTION_CROSS + endgroup + + covergroup cg_resolution_excl with function sample (logic [2:0] value, bit side); + cp: coverpoint value { + `RESOLUTION_BINS + ignore_bins unused = {5}; + } + `RESOLUTION_CROSS + endgroup + + // Real bin values keep the integral values they contain (IEEE 1800-2023 19.5.7). + covergroup cg_real_values with function sample (bit [2:0] value); + cp: coverpoint value { + bins two = {2.0}; + bins fraction = {2.5}; + bins far = {100.0}; + bins middle = {[3.5 : 5.5]}; + bins high = {[6.5 : 100.0]}; + bins low = {[-5.0 : 0.5]}; + ignore_bins ignored = {1.0}; + } + plain: coverpoint value { + bins two = {2.0}; + bins middle = {[3.5 : 5.5]}; + } + endgroup + + covergroup cg_real_point with function sample (real value); + cp: coverpoint value { + bins low = {[0 : 1]}; + bins middle = {[1.5 : 2.5]}; + bins three = {3.0}; + } + endgroup + + // More values than one constructor call describes + covergroup cg_many_values with function sample (bit [9:0] value); + cp: coverpoint value { + bins odd = { + 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, + 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, + 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, + 91, 93, 95, 97, 99, 101, 103, 105, 107, 109, 111, 113, 115, 117, 119, + 121, 123, 125, 127, 129, 131, 133, 135, 137, 139, 141, 143, 145, 147, 149, + 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 171, 173, 175, 177, 179, + 181, 183, 185, 187, 189, 191, 193, 195, 197, 199, 201, 203, 205, 207, 209, + 211, 213, 215, 217, 219, 221, 223, 225, 227, 229, 231, 233, 235, 237, 239, + 241, 243, 245, 247, 249, 251, 253, 255, 257, 259, 261, 263, 265, 267, 269, + 271, 273, 275, 277, 279, 281, 283, 285, 287, 289, 291, 293, 295, 297, 299, + 301, 303, 305, 307, 309, 311, 313, 315, 317, 319, 321, 323, 325, 327, 329, + 331, 333, 335, 337, 339, 341, 343, 345, 347, 349, 351, 353, 355, 357, 359, + 361, 363, 365, 367, 369, 371, 373, 375, 377, 379, 381, 383, 385, 387, 389, + 391, 393, 395, 397, 399, 401, 403, 405, 407, 409, 411, 413, 415, 417, 419, + 421, 423, 425, 427, 429, 431, 433, 435, 437, 439, 441, 443, 445, 447, 449, + 451, 453, 455, 457, 459, 461, 463, 465, 467, 469, 471, 473, 475, 477, 479, + 481, 483, 485, 487, 489, 491, 493, 495, 497, 499, 501, 503, 505, 507, 509, + 511, 513, 515, 517, 519, 521, 523, 525, 527, 529, 531, 533, 535, 537, 539, + 541, 543, 545, 547, 549, 551, 553, 555, 557, 559, 561, 563, 565, 567, 569, + 571, 573, 575, 577, 579, 581, 583, 585, 587, 589, 591, 593, 595, 597, 599 + }; + ignore_bins ignored = {0}; + } + endgroup + + cg_partition partition_cov = new; + cg_explicit explicit_cov = new; + cg_cross cross_cov = new; + cg_wide_cross wide_cross_cov = new; + cg_empty empty_cov = new; + cg_signed signed_cov = new; + cg_wide wide_cov = new; + cg_wild wild_cov = new; + cg_wild_empty wild_empty_cov = new; + cg_projection projection_cov = new; + cg_source_width source_width_cov = new; + cg_unsigned_pattern unsigned_pattern_cov = new; + cg_zero_limit zero_limit_cov = new; + cg_signed_wide signed_wide_cov = new; + cg_pattern_carry pattern_carry_cov = new; + cg_resolution resolution_cov = new; + cg_resolution_excl resolution_excl_cov = new; + cg_real_values real_values_cov = new; + cg_real_point real_point_cov = new; + cg_many_values many_values_cov = new; + + always @(posedge clk) begin + if (cyc == 0) begin + partition_cov.sample(2); + partition_cov.sample(4); + partition_cov.sample(7); + `checkr(partition_cov.get_inst_coverage(), 100.0); + partition_cov.sample(3); + explicit_cov.sample(2); + explicit_cov.sample(3); + explicit_cov.sample(4); + `checkr(explicit_cov.get_inst_coverage(), 100.0); + empty_cov.sample(0, 1); + empty_cov.sample(3, 2); + `checkr(empty_cov.get_inst_coverage(), 50.0); + signed_cov.sample(-22); + signed_cov.sample(63); + `checkr(signed_cov.get_inst_coverage(), 100.0); + signed_cov.sample(-64); + wide_cov.sample(65'h0_8000000000000000); + wide_cov.sample(65'h1_0000000000000000); + wide_cov.sample(65'h1_ffffffffffffffff); + `checkr(wide_cov.get_inst_coverage(), 100.0); + wide_cov.sample(0); + wild_cov.sample(1); + wild_cov.sample(65'h1_ffffffffffffffff); + wild_cov.sample(0); + `checkr(wild_cov.get_inst_coverage(), 100.0); + wild_empty_cov.sample(0); + wild_empty_cov.sample(1); + wild_empty_cov.sample(65'h1_fffffffffffffffe); + wild_empty_cov.sample(65'h1_ffffffffffffffff); + projection_cov.sample(-1); + `checkr(projection_cov.get_inst_coverage(), 50.0); + projection_cov.sample(1); + `checkr(projection_cov.get_inst_coverage(), 100.0); + source_width_cov.sample(-4); + source_width_cov.sample(2); + `checkr(source_width_cov.get_inst_coverage(), 0.0); + source_width_cov.sample(-2); + source_width_cov.sample(0); + `checkr(source_width_cov.get_inst_coverage(), 100.0); + unsigned_pattern_cov.sample(-1); + `checkr(unsigned_pattern_cov.get_inst_coverage(), 100.0); + zero_limit_cov.sample(0); + zero_limit_cov.sample(1); + signed_wide_cov.sample(-1, 0); + signed_wide_cov.sample(-1, 1); + signed_wide_cov.sample(0, 0); + signed_wide_cov.sample(65'sh0_ffffffffffffffff, 1); + signed_wide_cov.sample(65'sh1_0000000000000000, 0); + for (int value = 0; value < 8; ++value) begin + if (value != 5) begin + resolution_cov.sample(3'(value), 0); + resolution_cov.sample(3'(value), 1); + resolution_excl_cov.sample(3'(value), 0); + resolution_excl_cov.sample(3'(value), 1); + end + real_values_cov.sample(3'(value)); + end + `checkr(resolution_cov.get_inst_coverage(), 100.0); + `checkr(resolution_excl_cov.get_inst_coverage(), 100.0); + `checkr(real_values_cov.get_inst_coverage(), 100.0); + real_point_cov.sample(1.25); + `checkr(real_point_cov.get_inst_coverage(), 0.0); + real_point_cov.sample(0.5); + real_point_cov.sample(2.0); + real_point_cov.sample(3.0); + `checkr(real_point_cov.get_inst_coverage(), 100.0); + many_values_cov.sample(2); + `checkr(many_values_cov.get_inst_coverage(), 0.0); + many_values_cov.sample(599); + many_values_cov.sample(1); + `checkr(many_values_cov.get_inst_coverage(), 100.0); + end + if (cyc < 8) begin + if (cyc == 0 || cyc == 1 || cyc == 5 || cyc == 6) partition_cov.sample(3'(cyc)); + if (cyc == 0 || cyc == 1 || cyc >= 5) explicit_cov.sample(3'(cyc)); + cross_cov.sample(cyc < 4 ? 4'd4 : 4'd8, 2'(cyc), 1'(cyc)); + end + if (cyc == 8) cross_cov.sample(8, 2, 1); + if (cyc < 16) pattern_carry_cov.sample(4'(cyc), 4'(cyc), 1'(cyc / 2)); + if (cyc < 65) begin + wide_cross_cov.sample(7'(cyc), 0, 1); + wide_cross_cov.sample(7'(cyc), 1, 1); + end + else begin + wide_cross_cov.sample(1, 0, 0); + $write("*-* All Finished *-*\n"); + $finish; + end + ++cyc; + end +endmodule diff --git a/test_regress/t/t_covergroup_auto_exclusions_asan.py b/test_regress/t/t_covergroup_auto_exclusions_asan.py new file mode 100755 index 000000000..6641036cc --- /dev/null +++ b/test_regress/t/t_covergroup_auto_exclusions_asan.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap +import coverage_covergroup_common + +test.scenarios('vlt') +test.top_filename = 't/t_covergroup_auto_exclusions.v' +test.golden_filename = 't/t_covergroup_auto_exclusions.out' + +if test.tsan: + test.skip("ThreadSanitizer not compatible with AddressSanitizer\n") + +# Construction-time value analysis and dynamic cross layouts under AddressSanitizer +coverage_covergroup_common.run( + test, verilator_flags2=['--timing', '-CFLAGS -fsanitize=address -LDFLAGS -fsanitize=address']) + +test.passes() diff --git a/test_regress/t/t_covergroup_auto_exclusions_off.py b/test_regress/t/t_covergroup_auto_exclusions_off.py new file mode 100755 index 000000000..6f8ac0c31 --- /dev/null +++ b/test_regress/t/t_covergroup_auto_exclusions_off.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt_all') + +test.top_filename = 't/t_covergroup_auto_exclusions.v' + +test.compile(verilator_flags2=['--timing'], threads=(2 if test.vltmt else 1)) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_covergroup_autobins_bad.out b/test_regress/t/t_covergroup_autobins_bad.out index 0e8c40794..86d8155c1 100644 --- a/test_regress/t/t_covergroup_autobins_bad.out +++ b/test_regress/t/t_covergroup_autobins_bad.out @@ -11,14 +11,6 @@ : ... note: In instance 't' 32 | bins auto[1001]; | ^~~~ -%Error: t/t_covergroup_autobins_bad.v:44:26: Non-constant expression in bin value list; values must be constants (IEEE 1800-2023 19.5) - : ... note: In instance 't' - 44 | ignore_bins ign = {size_var}; - | ^~~~~~~~ -%Error: t/t_covergroup_autobins_bad.v:45:32: Non-constant expression in bin range; range bounds must be constants (IEEE 1800-2023 19.5) - : ... note: In instance 't' - 45 | ignore_bins ign_range = {[0:size_var]}; - | ^ %Error: t/t_covergroup_autobins_bad.v:39:12: Non-constant expression in array bins range; range bounds must be constants (IEEE 1800-2023 19.5) : ... note: In instance 't' 39 | bins b[] = {[size_var:size_var]}; @@ -43,16 +35,16 @@ : ... note: In instance 't' 44 | ignore_bins ign = {size_var}; | ^~~~~~~~ +%Error: t/t_covergroup_autobins_bad.v:45:32: Non-constant expression in bin range; range bounds must be constants (IEEE 1800-2023 19.5) + : ... note: In instance 't' + 45 | ignore_bins ign_range = {[0:size_var]}; + | ^ %Warning-COVERIGN: t/t_covergroup_autobins_bad.v:52:25: Ignoring unsupported: non-constant 'option.at_least'; using default value : ... note: In instance 't' 52 | option.at_least = size_var; | ^~~~~~~~ ... For warning description see https://verilator.org/warn/COVERIGN?v=latest ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. -%Error: t/t_covergroup_autobins_bad.v:62:31: Non-constant expression in bin range; range bounds must be constants (IEEE 1800-2023 19.5) - : ... note: In instance 't' - 62 | ignore_bins ign_nclo = {[size_var:4]}; - | ^ %Error: t/t_covergroup_autobins_bad.v:59:20: Four-state (x/z) value in bin range bound; range bounds must be two-state constants : ... note: In instance 't' 59 | bins b_xz = {[4'bxxxx:4'hF]}; @@ -65,6 +57,10 @@ : ... note: In instance 't' 61 | ignore_bins ign_xz_hi = {[4'h0:4'bzzzz]}; | ^ +%Error: t/t_covergroup_autobins_bad.v:62:31: Non-constant expression in bin range; range bounds must be constants (IEEE 1800-2023 19.5) + : ... note: In instance 't' + 62 | ignore_bins ign_nclo = {[size_var:4]}; + | ^ %Error: t/t_covergroup_autobins_bad.v:63:23: Non-constant expression in bin range; range bounds must be constants (IEEE 1800-2023 19.5) : ... note: In instance 't' 63 | bins b_nc_ub = {[size_var:$]}; @@ -185,20 +181,12 @@ : ... note: In instance 't' 161 | auto_only: cross cp_a, cp_b, cp_c, cp_d, cp_e, cp_f; | ^~~~~ -%Error: t/t_covergroup_autobins_bad.v:167:34: Non-constant expression in bin value list; values must be constants (IEEE 1800-2023 19.5) +%Warning-COVERIGN: t/t_covergroup_autobins_bad.v:172:9: Unsupported: cross coverage with more than 2^32-1 tuples. + : ... note: In instance 't' + 172 | xc: cross cp_a, cp_b, cp_c, cp_d, cp_e, cp_f; + | ^~~~~ +%Error: t/t_covergroup_autobins_bad.v:178:34: Non-constant expression in bin range; values must be constants (IEEE 1800-2023 19.5) : ... note: In instance 't' - 167 | ignore_bins nonconstant = {size_var}; + 178 | ignore_bins nonconstant = {size_var}; | ^~~~~~~~ -%Error: t/t_covergroup_autobins_bad.v:167:34: Non-constant expression in bin range; values must be constants (IEEE 1800-2023 19.5) - : ... note: In instance 't' - 167 | ignore_bins nonconstant = {size_var}; - | ^~~~~~~~ -%Warning-COVERIGN: t/t_covergroup_autobins_bad.v:171:23: Unsupported: non-constant or non-integral 'intersect' value, or four-state range bound. - : ... note: In instance 't' - 171 | bins selected = binsof(cp_a) intersect {0}; - | ^~~~~~ -%Warning-COVERIGN: t/t_covergroup_autobins_bad.v:210:23: Unsupported: 'intersect' exclusion matching exceeds the selection work limit. - : ... note: In instance 't' - 210 | bins selected = binsof(cp_a.whole) intersect {[0:30'h3fffffff]} && binsof(cp_b); - | ^~~~~~ %Error: Exiting due to diff --git a/test_regress/t/t_covergroup_autobins_bad.py b/test_regress/t/t_covergroup_autobins_bad.py index ef7407f24..75c2791e9 100755 --- a/test_regress/t/t_covergroup_autobins_bad.py +++ b/test_regress/t/t_covergroup_autobins_bad.py @@ -9,7 +9,7 @@ import vltest_bootstrap -test.scenarios('vlt') +test.scenarios('linter') test.lint(expect_filename=test.golden_filename, fails=True) diff --git a/test_regress/t/t_covergroup_autobins_bad.v b/test_regress/t/t_covergroup_autobins_bad.v index 44804af79..d80663b0e 100644 --- a/test_regress/t/t_covergroup_autobins_bad.v +++ b/test_regress/t/t_covergroup_autobins_bad.v @@ -42,7 +42,7 @@ module t; bins b_range2 = {[0:size_var]}; // non-constant regular bin range (rhs non-const) bins b2 = {size_var}; // non-constant simple bin value ignore_bins ign = {size_var}; // non-constant ignore_bins value - ignore_bins ign_range = {[0:size_var]}; // non-constant ignore_bins range (rhs non-const) + ignore_bins ign_range = {[0:size_var]}; } endgroup @@ -59,7 +59,7 @@ module t; bins b_xz = {[4'bxxxx:4'hF]}; // four-state lower bound (match-code path) ignore_bins ign_xz_lo = {[4'bxxxx:4'hF]}; // four-state lower bound (range-enum path) ignore_bins ign_xz_hi = {[4'h0:4'bzzzz]}; // four-state upper bound (range-enum path) - ignore_bins ign_nclo = {[size_var:4]}; // non-constant lower bound + ignore_bins ign_nclo = {[size_var:4]}; bins b_nc_ub = {[size_var:$]}; // non-constant lower bound, open-ended '$' upper bins b_xz_ub = {[4'bxxxx:$]}; // four-state lower bound, open-ended '$' upper bins b_xz_arr[] = {[4'bxxxx:4'hF]}; // four-state lower bound (array-bins path) @@ -161,6 +161,17 @@ module t; auto_only: cross cp_a, cp_b, cp_c, cp_d, cp_e, cp_f; endgroup + // Live bins only shrink a runtime cross, so its declared product is checked the same way. + covergroup cgx_dynamic_large; + cp_a: coverpoint cp_wide {ignore_bins removed = {0};} + cp_b: coverpoint cp_wide; + cp_c: coverpoint cp_wide; + cp_d: coverpoint cp_wide; + cp_e: coverpoint cp_wide; + cp_f: coverpoint cp_wide; + xc: cross cp_a, cp_b, cp_c, cp_d, cp_e, cp_f; + endgroup + covergroup cgx_binsof_excluded; cp_a: coverpoint cp_expr { bins normal = {0}; @@ -172,45 +183,6 @@ module t; } endgroup - logic [29:0] complex_value; - localparam logic [29:0] ANY = 30'bx; - - covergroup cgx_binsof_complex; - cp_a: coverpoint complex_value { - bins whole = {[0:30'h3fffffff]}; - // Six pigeons in five holes: a deliberately hard union of excluded assignments. - wildcard ignore_bins no_hole = { - (ANY & ~30'h0000001f), (ANY & ~30'h000003e0), (ANY & ~30'h00007c00), - (ANY & ~30'h000f8000), (ANY & ~30'h01f00000), (ANY & ~30'h3e000000) - }; - wildcard ignore_bins shared_hole = { - (ANY | 30'h00000021), (ANY | 30'h00000401), (ANY | 30'h00008001), (ANY | 30'h00100001), - (ANY | 30'h02000001), (ANY | 30'h00000420), (ANY | 30'h00008020), (ANY | 30'h00100020), - (ANY | 30'h02000020), (ANY | 30'h00008400), (ANY | 30'h00100400), (ANY | 30'h02000400), - (ANY | 30'h00108000), (ANY | 30'h02008000), (ANY | 30'h02100000), (ANY | 30'h00000042), - (ANY | 30'h00000802), (ANY | 30'h00010002), (ANY | 30'h00200002), (ANY | 30'h04000002), - (ANY | 30'h00000840), (ANY | 30'h00010040), (ANY | 30'h00200040), (ANY | 30'h04000040), - (ANY | 30'h00010800), (ANY | 30'h00200800), (ANY | 30'h04000800), (ANY | 30'h00210000), - (ANY | 30'h04010000), (ANY | 30'h04200000), (ANY | 30'h00000084), (ANY | 30'h00001004), - (ANY | 30'h00020004), (ANY | 30'h00400004), (ANY | 30'h08000004), (ANY | 30'h00001080), - (ANY | 30'h00020080), (ANY | 30'h00400080), (ANY | 30'h08000080), (ANY | 30'h00021000), - (ANY | 30'h00401000), (ANY | 30'h08001000), (ANY | 30'h00420000), (ANY | 30'h08020000), - (ANY | 30'h08400000), (ANY | 30'h00000108), (ANY | 30'h00002008), (ANY | 30'h00040008), - (ANY | 30'h00800008), (ANY | 30'h10000008), (ANY | 30'h00002100), (ANY | 30'h00040100), - (ANY | 30'h00800100), (ANY | 30'h10000100), (ANY | 30'h00042000), (ANY | 30'h00802000), - (ANY | 30'h10002000), (ANY | 30'h00840000), (ANY | 30'h10040000), (ANY | 30'h10800000), - (ANY | 30'h00000210), (ANY | 30'h00004010), (ANY | 30'h00080010), (ANY | 30'h01000010), - (ANY | 30'h20000010), (ANY | 30'h00004200), (ANY | 30'h00080200), (ANY | 30'h01000200), - (ANY | 30'h20000200), (ANY | 30'h00084000), (ANY | 30'h01004000), (ANY | 30'h20004000), - (ANY | 30'h01080000), (ANY | 30'h20080000), (ANY | 30'h21000000) - }; - } - cp_b: coverpoint cp_expr; - xc: cross cp_a, cp_b { - bins selected = binsof(cp_a.whole) intersect {[0:30'h3fffffff]} && binsof(cp_b); - } - endgroup - covergroup cgx_binsof_many_values; cp_a: coverpoint cp_wide { bins whole[] = {[0:767]}; @@ -255,8 +227,8 @@ module t; cgx_arr_open cgx_arr_open_inst = new; cgx_binsof cgx_binsof_inst = new; cgx_binsof_large cgx_binsof_large_inst = new; + cgx_dynamic_large cgx_dynamic_large_inst = new; cgx_binsof_excluded cgx_binsof_excluded_inst = new; - cgx_binsof_complex cgx_binsof_complex_inst = new; cgx_binsof_many_values cgx_binsof_many_values_inst = new; initial $finish; diff --git a/test_regress/t/t_covergroup_binsof.out b/test_regress/t/t_covergroup_binsof.out index f360ab755..246fb688a 100644 --- a/test_regress/t/t_covergroup_binsof.out +++ b/test_regress/t/t_covergroup_binsof.out @@ -16,7 +16,7 @@ cg_binsof.cp_a.arrayed[1]: 2 cg_binsof.cp_a.high: 2 cg_binsof.cp_a.ignored [ignore]: 2 cg_binsof.cp_a.low: 4 -cg_binsof.cp_a.rest [default]: 8 +cg_binsof.cp_a.rest [default]: 6 cg_binsof.cp_b.either: 18 cg_binsof.cp_b.high: 9 cg_binsof.cp_b.low: 9 diff --git a/test_regress/t/t_covergroup_binsof_ops.out b/test_regress/t/t_covergroup_binsof_ops.out index ac51a9808..00d0014af 100644 --- a/test_regress/t/t_covergroup_binsof_ops.out +++ b/test_regress/t/t_covergroup_binsof_ops.out @@ -11,7 +11,6 @@ cg_excluded_four_state.cp_a.states: 0 cg_excluded_four_state.cp_a.xstate [ignore]: 0 cg_excluded_four_state.cp_b.auto_0: 0 cg_excluded_four_state.cp_b.auto_1: 0 -cg_excluded_four_state.selected.kept [cross]: 0 cg_excluded_four_state.selected.numeric [cross]: 0 cg_excluded_many.cp_a.whole: 2 cg_excluded_many.cp_a.zero_bit [ignore]: 0 @@ -103,12 +102,8 @@ cg_fixed_words.selected.all_values [cross]: 16 cg_fixed_words.sparse.all_values [cross]: 16 cg_fixed_words.sparse.subset [cross]: 4 cg_four_state.cp_a.known: 0 -cg_four_state.cp_a.xstate: 0 -cg_four_state.cp_a.zstate: 0 cg_four_state.cp_b.auto_0: 0 cg_four_state.cp_b.auto_1: 0 -cg_four_state.selected.exact_x [cross]: 0 -cg_four_state.selected.exact_z [cross]: 0 cg_four_state.selected.not_x [cross]: 0 cg_guards.cp_a.zero: 8 cg_guards.cp_b.zero: 8 diff --git a/test_regress/t/t_covergroup_binsof_ops.v b/test_regress/t/t_covergroup_binsof_ops.v index 96d3a7a72..f86f8f6d2 100644 --- a/test_regress/t/t_covergroup_binsof_ops.v +++ b/test_regress/t/t_covergroup_binsof_ops.v @@ -303,7 +303,8 @@ module t; } endgroup - // Check four-state bin identities without relying on four-state sampling. + // Values with x or z bits do not participate in bins or selections (IEEE 1800-2023 19.5.7), + // so the x/z bins and the selections of x/z values have no values and are not reported. covergroup cg_four_state with function sample (logic [2:0] a, bit b); cp_a: coverpoint a { bins known = {3'b001}; diff --git a/test_regress/t/t_covergroup_cross.out b/test_regress/t/t_covergroup_cross.out index 687d59256..16d4ad5a4 100644 --- a/test_regress/t/t_covergroup_cross.out +++ b/test_regress/t/t_covergroup_cross.out @@ -57,9 +57,6 @@ cg5.cp_addr.addr0: 1 cg5.cp_addr.addr1: 1 cg5.cp_cmd.read: 1 cg5.cp_cmd.write: 1 -cg_arr_4state.a4.av[0]_x_read [cross]: 1 -cg_arr_4state.a4.av[0]_x_write [cross]: 1 -cg_arr_4state.cp_addr.av[0]: 2 cg_arr_4state.cp_cmd.read: 1 cg_arr_4state.cp_cmd.write: 1 cg_arr_range.ar.av[0]_x_read [cross]: 1 @@ -128,11 +125,8 @@ cg_ignore.cross_ab.a0_x_read [cross]: 1 cg_ignore.cross_ab.a0_x_write [cross]: 1 cg_ignore.cross_ab.a1_x_read [cross]: 1 cg_ignore.cross_ab.a1_x_write [cross]: 1 -cg_inv.cp_addr.inv: 0 cg_inv.cp_cmd.read: 1 cg_inv.cp_cmd.write: 1 -cg_inv.iv.inv_x_read [cross]: 0 -cg_inv.iv.inv_x_write [cross]: 0 cg_mixed.ab.addr0_x_read [cross]: 1 cg_mixed.ab.addr0_x_write [cross]: 1 cg_mixed.ab.addr1_x_read [cross]: 1 diff --git a/test_regress/t/t_covergroup_cross.v b/test_regress/t/t_covergroup_cross.v index b1ca88426..1078db87b 100644 --- a/test_regress/t/t_covergroup_cross.v +++ b/test_regress/t/t_covergroup_cross.v @@ -171,7 +171,8 @@ module t; // Crossed coverpoint with a four-state literal in a non-wildcard array bin // (bins av[] = {2'b0x}): LRM 1800-2023 19.5.4 permits 4-state values in a bin definition. // The hit-list sizing cannot statically analyze a 4-state value, so it falls back to the - // safe slot count. Under Verilator's 2-state simulation the value matches addr=0. + // safe slot count. A value with x or z bits does not participate (IEEE 1800-2023 19.5.7), + // so av[0] and its cross bins have no values and leave the coverage computation. covergroup cg_arr_4state; cp_addr: coverpoint addr {bins av[] = {2'b0x};} cp_cmd: coverpoint cmd {bins read = {0}; bins write = {1};} @@ -204,9 +205,10 @@ module t; orc: cross cp_addr, cp_cmd; endgroup - // Crossed coverpoint with an inverted range bin (lo bound > hi bound): the bin matches no - // value, so the hit-list sizing rejects it (lo > hi) and falls back to the safe slot count. - // The 'inv' bin and its cross bins are therefore never hit (coverage stays at 40%). + // Crossed coverpoint with an inverted range bin (lo bound > hi bound): the range is empty + // (IEEE 1800-2023 11.4.13), so the hit-list sizing rejects it (lo > hi) and falls back to the + // safe slot count. A bin without values, and its cross bins, leave the coverage computation + // (IEEE 1800-2023 19.11.1). covergroup cg_inv; cp_addr: coverpoint addr {bins inv = {[3 : 0]};} // inverted -> never matches cp_cmd: coverpoint cmd {bins read = {0}; bins write = {1};} @@ -486,11 +488,11 @@ module t; addr = 0; cmd = 1; cg_wild_solo_inst.sample(); `checkr(cg_wild_solo_inst.get_inst_coverage(), 100.0); // 5/5 - // Sample cg_arr_4state: 4-state literal bin {2'b0x} matches addr=0 (2-state sim); cross 1x2 - // cg_arr_4state: 1+2+2=5 bins; sample both cmd values -> 100% + // Sample cg_arr_4state: 4-state literal bin {2'b0x} has no value, so addr=0 hits no bin + // cg_arr_4state: 0+2+0=2 bins; sample both cmd values -> 100% addr = 0; cmd = 0; cg_arr_4state_inst.sample(); addr = 0; cmd = 1; cg_arr_4state_inst.sample(); - `checkr(cg_arr_4state_inst.get_inst_coverage(), 100.0); // 5/5 + `checkr(cg_arr_4state_inst.get_inst_coverage(), 100.0); // 2/2 // Sample cg_overlap: overlapping range bins lo={0,1}, hi={1,2}; cross 2x2 // cg_overlap: 2+2+4=8 bins; cover lo/hi via addr 0 and 2, plus addr=1 double-hits both @@ -517,11 +519,11 @@ module t; addr = 2; cmd = 1; cg_openrange_inst.sample(); // hi x write `checkr(cg_openrange_inst.get_inst_coverage(), 100.0); // 8/8 - // Sample cg_inv: inverted range bin never matches; only cmd bins are hittable - // cg_inv: 1+2+2=5 bins; inv and its 2 cross bins never hit -> 2/5=40% + // Sample cg_inv: the empty inverted range bin and its cross bins are not counted + // cg_inv: 0+2+0=2 bins -> read + write give 2/2=100% addr = 0; cmd = 0; cg_inv_inst.sample(); // read addr = 1; cmd = 1; cg_inv_inst.sample(); // write - `checkr(cg_inv_inst.get_inst_coverage(), 40.0); // 2/5: read + write only + `checkr(cg_inv_inst.get_inst_coverage(), 100.0); // 2/2: read + write only // Sample cg_noNormal: coverpoint has no Normal bins; cross product is empty // cg_noNormal: 0+2+0=2 bins (cmd only); both hit -> 100% diff --git a/test_regress/t/t_covergroup_cross_ref_bad.out b/test_regress/t/t_covergroup_cross_ref_bad.out index 608cba4c3..abb53f461 100644 --- a/test_regress/t/t_covergroup_cross_ref_bad.out +++ b/test_regress/t/t_covergroup_cross_ref_bad.out @@ -19,4 +19,34 @@ : ... note: In instance 't' 19 | bins invalid_right = cx || other; | ^~~~~ +%Error: t/t_covergroup_cross_ref_bad.v:33:26: Cross selection 'other' may only name its enclosing cross 'cx' (IEEE 1800-2023 19.6.1.2). + : ... note: In instance 't' + 33 | bins wrong_cross = other; + | ^~~~~ +%Error: t/t_covergroup_cross_ref_bad.v:34:27: Cross selection 'missing' may only name its enclosing cross 'cx' (IEEE 1800-2023 19.6.1.2). + : ... note: In instance 't' + 34 | bins invalid_left = missing && cx; + | ^~~~~~~ +%Error: t/t_covergroup_cross_ref_bad.v:35:34: Cross selection 'missing' may only name its enclosing cross 'cx' (IEEE 1800-2023 19.6.1.2). + : ... note: In instance 't' + 35 | bins invalid_right = cx || missing; + | ^~~~~~~ +%Error: t/t_covergroup_cross_ref_bad.v:36:26: binsof coverpoint 'cp_other' is not an item of cross 'cx' (IEEE 1800-2023 19.6.1). + : ... note: In instance 't' + 36 | bins wrong_point = binsof (cp_other); + | ^~~~~~ +%Error: t/t_covergroup_cross_ref_bad.v:37:24: Cannot find bin 'missing' in coverpoint 'cp_a' (IEEE 1800-2023 19.6.1). + : ... note: In instance 't' + 37 | bins wrong_bin = binsof (cp_a.missing); + | ^~~~~~ +%Error: t/t_covergroup_cross_ref_bad.v:39:7: Duplicate cross bin 'duplicate' (IEEE 1800-2023 19.6.1). + : ... note: In instance 't' + 39 | bins duplicate = binsof (cp_b); + | ^~~~ +%Warning-COVERIGN: t/t_covergroup_cross_ref_bad.v:40:26: Unsupported: non-constant or non-integral 'intersect' value, or four-state range bound. + : ... note: In instance 't' + 40 | bins nonconstant = binsof (cp_a) intersect {limit_value}; + | ^~~~~~ + ... For warning description see https://verilator.org/warn/COVERIGN?v=latest + ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. %Error: Exiting due to diff --git a/test_regress/t/t_covergroup_cross_ref_bad.py b/test_regress/t/t_covergroup_cross_ref_bad.py index 0f63cc666..1f43f1055 100755 --- a/test_regress/t/t_covergroup_cross_ref_bad.py +++ b/test_regress/t/t_covergroup_cross_ref_bad.py @@ -9,8 +9,8 @@ import vltest_bootstrap -test.scenarios('vlt') +test.scenarios('linter') -test.compile(verilator_flags2=['--coverage'], fails=True, expect_filename=test.golden_filename) +test.lint(verilator_flags2=['--coverage'], fails=True, expect_filename=test.golden_filename) test.passes() diff --git a/test_regress/t/t_covergroup_cross_ref_bad.v b/test_regress/t/t_covergroup_cross_ref_bad.v index 04b3442ba..ee664ce86 100644 --- a/test_regress/t/t_covergroup_cross_ref_bad.v +++ b/test_regress/t/t_covergroup_cross_ref_bad.v @@ -20,7 +20,29 @@ module t; } endgroup + int limit_value; + + covergroup cg_dynamic; + cp_a: coverpoint value { + ignore_bins ignored = {0}; + } + cp_b: coverpoint value; + cp_other: coverpoint value; + other: cross cp_a, cp_b; + cx: cross cp_a, cp_b{ + bins wrong_cross = other; + bins invalid_left = missing && cx; + bins invalid_right = cx || missing; + bins wrong_point = binsof (cp_other); + bins wrong_bin = binsof (cp_a.missing); + bins duplicate = cx; + bins duplicate = binsof (cp_b); + bins nonconstant = binsof (cp_a) intersect {limit_value}; + } + endgroup + cg cov = new; + cg_dynamic dynamic_cov = new; initial $finish; endmodule diff --git a/test_regress/t/t_covergroup_default_bins.out b/test_regress/t/t_covergroup_default_bins.out index e034545b8..fa8d0f3ac 100644 --- a/test_regress/t/t_covergroup_default_bins.out +++ b/test_regress/t/t_covergroup_default_bins.out @@ -5,10 +5,10 @@ cg2.cp_only_default.all [default]: 4 cg3.data.bad [ignore]: 1 cg3.data.err [illegal]: 0 cg3.data.normal: 2 -cg3.data.other [default]: 2 +cg3.data.other [default]: 1 cg4.cp_idx.auto_0: 1 cg4.cp_idx.auto_1: 1 -cg4.cp_idx.auto_2: 1 +cg4.cp_idx.auto_3: 1 cg4.cp_idx.skip [ignore]: 0 cg5.cp_data64.auto[0]: 2 cg5.cp_data64.auto[1]: 0 diff --git a/test_regress/t/t_covergroup_default_bins.v b/test_regress/t/t_covergroup_default_bins.v index c97cf8f66..e64c938b7 100644 --- a/test_regress/t/t_covergroup_default_bins.v +++ b/test_regress/t/t_covergroup_default_bins.v @@ -59,7 +59,7 @@ module t; endgroup // Auto-bins on a small range with one value excluded by ignore_bins - - // when the range is small enough, one auto-bin per valid value is created; the excluded value is skipped. + // the empty auto_2 bin is omitted without renumbering the remaining bins. covergroup cg4; cp_idx: coverpoint idx { ignore_bins skip = {2}; // value 2 excluded; auto-bins created for 0,1,3 diff --git a/test_regress/t/t_covergroup_excl_protect_ids.py b/test_regress/t/t_covergroup_excl_protect_ids.py new file mode 100755 index 000000000..2f3997945 --- /dev/null +++ b/test_regress/t/t_covergroup_excl_protect_ids.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt_all') + +test.top_filename = 't/t_covergroup_auto_exclusions.v' + +test.compile(verilator_flags2=[ + '--coverage-user', '--protect-ids', '--protect-key AUTO_EXCLUSION_KEY', '-Wno-INSECURE' +], + threads=(2 if test.vltmt else 1)) + +test.execute() + +test.file_grep_not( + test.coverage_filename, + r'cg_partition|cg_explicit|cg_projection|cg_wide_cross|t_covergroup_auto_exclusions|enabled_bin' +) + +test.passes() diff --git a/test_regress/t/t_covergroup_exclusions_unsup.out b/test_regress/t/t_covergroup_exclusions_unsup.out new file mode 100644 index 000000000..763782ea1 --- /dev/null +++ b/test_regress/t/t_covergroup_exclusions_unsup.out @@ -0,0 +1,24 @@ +%Error-UNSUPPORTED: t/t_covergroup_exclusions_unsup.v:14:20: Unsupported: non-integral value in a coverage bin of an integral coverpoint. + : ... note: In instance 't' + 14 | bins text = {TEXT}; + | ^~~~ + ... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest +%Error-UNSUPPORTED: t/t_covergroup_exclusions_unsup.v:21:29: Unsupported: non-integral value in a coverage bin of an integral coverpoint. + : ... note: In instance 't' + 21 | wildcard bins text = {TEXT}; + | ^~~~ +%Error-UNSUPPORTED: t/t_covergroup_exclusions_unsup.v:27:25: Unsupported: non-integral value in a transition bin of a coverpoint with exclusions. + : ... note: In instance 't' + 27 | bins text = (1 => TEXT); + | ^~~~ +%Warning-COVERIGN: t/t_covergroup_exclusions_unsup.v:40:59: Unsupported: non-constant or non-integral 'intersect' value, or four-state range bound. + : ... note: In instance 't' + 40 | static_cross: cross cp_real, cp_plain{bins selected = binsof (cp_real) intersect {1};} + | ^~~~~~ + ... For warning description see https://verilator.org/warn/COVERIGN?v=latest + ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. +%Warning-COVERIGN: t/t_covergroup_exclusions_unsup.v:41:62: Unsupported: non-constant or non-integral 'intersect' value, or four-state range bound. + : ... note: In instance 't' + 41 | dynamic_cross: cross cp_real, cp_dynamic{bins selected = binsof (cp_real) intersect {1};} + | ^~~~~~ +%Error: Exiting due to diff --git a/test_regress/t/t_covergroup_exclusions_unsup.py b/test_regress/t/t_covergroup_exclusions_unsup.py new file mode 100755 index 000000000..95871adf7 --- /dev/null +++ b/test_regress/t/t_covergroup_exclusions_unsup.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.compile(verilator_flags2=['--timing'], fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_covergroup_exclusions_unsup.v b/test_regress/t/t_covergroup_exclusions_unsup.v new file mode 100644 index 000000000..93f3cb736 --- /dev/null +++ b/test_regress/t/t_covergroup_exclusions_unsup.v @@ -0,0 +1,50 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t; + bit [3:0] value; + real real_value; + localparam string TEXT = "a"; + + covergroup cg_values; + cp: coverpoint value { + bins text = {TEXT}; + ignore_bins ignored = {0}; + } + endgroup + + covergroup cg_wild; + cp: coverpoint value { + wildcard bins text = {TEXT}; + } + endgroup + + covergroup cg_transition; + cp: coverpoint value { + bins text = (1 => TEXT); + ignore_bins ignored = {0}; + } + endgroup + + covergroup cg_cross; + cp_real: coverpoint real_value { + bins one = {1.0}; + } + cp_plain: coverpoint value; + cp_dynamic: coverpoint value { + ignore_bins ignored = {0}; + } + static_cross: cross cp_real, cp_plain{bins selected = binsof (cp_real) intersect {1};} + dynamic_cross: cross cp_real, cp_dynamic{bins selected = binsof (cp_real) intersect {1};} + endgroup + + cg_values values_cov = new; + cg_wild wild_cov = new; + cg_transition transition_cov = new; + cg_cross cross_cov = new; + + initial $finish; +endmodule diff --git a/test_regress/t/t_covergroup_ignore_bins.out b/test_regress/t/t_covergroup_ignore_bins.out index 6a1cba188..c280ef0b4 100644 --- a/test_regress/t/t_covergroup_ignore_bins.out +++ b/test_regress/t/t_covergroup_ignore_bins.out @@ -17,6 +17,6 @@ cg2.cp_auto_ub.ub [ignore]: 2 cg2.cp_bounds.hi: 2 cg2.cp_bounds.lo: 2 cg2.cp_full.all: 4 -cg3.cp_auto_lb.auto_0: 1 -cg3.cp_auto_lb.auto_1: 1 +cg3.cp_auto_lb.auto_2: 1 +cg3.cp_auto_lb.auto_3: 1 cg3.cp_auto_lb.lb [ignore]: 1 diff --git a/test_regress/t/t_covergroup_ignore_bins.v b/test_regress/t/t_covergroup_ignore_bins.v index cc0930ecf..2f81b9912 100644 --- a/test_regress/t/t_covergroup_ignore_bins.v +++ b/test_regress/t/t_covergroup_ignore_bins.v @@ -27,7 +27,7 @@ module t; } endgroup - // cg2: ignore_bins using a range - auto-bins are created only for values not in the range. + // cg2: ignore_bins using a range - empty auto-bins are omitted after partitioning. // Also tests range-boundary conditions: when lo==0 or hi==maxVal, the range check simplifies. // Also tests ignore_bins with a transition list. covergroup cg2; @@ -87,10 +87,10 @@ module t; data2 = 0; cg3_inst.sample(); // lb (ignored) data2 = 2; - cg3_inst.sample(); // auto_0 + cg3_inst.sample(); // auto_2 `checkr(cg3_inst.get_inst_coverage(), 50.0); data2 = 3; - cg3_inst.sample(); // auto_1 + cg3_inst.sample(); // auto_3 `checkr(cg3_inst.get_inst_coverage(), 100.0); $write("*-* All Finished *-*\n"); diff --git a/test_regress/t/t_covergroup_limits.out b/test_regress/t/t_covergroup_limits.out new file mode 100644 index 000000000..67ebe78b9 --- /dev/null +++ b/test_regress/t/t_covergroup_limits.out @@ -0,0 +1,2 @@ +%Warning: t/t_covergroup_limits.v:42: Coverage bin exclusions exceed the decision-graph work limit; bin retained +*-* All Finished *-* diff --git a/test_regress/t/t_covergroup_limits.py b/test_regress/t/t_covergroup_limits.py new file mode 100755 index 000000000..be8e66996 --- /dev/null +++ b/test_regress/t/t_covergroup_limits.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt_all') + +test.compile(verilator_flags2=['--timing'], threads=(2 if test.vltmt else 1)) + +test.execute(expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_covergroup_limits.v b/test_regress/t/t_covergroup_limits.v new file mode 100644 index 000000000..cdfc66122 --- /dev/null +++ b/test_regress/t/t_covergroup_limits.v @@ -0,0 +1,84 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkr(gotv,expv) do if ((gotv) != (expv)) begin $write("%%Error: %s:%0d: got=%f exp=%f\n", `__FILE__, `__LINE__, (gotv), (expv)); `stop; end while (0); +// verilog_format: on + +// A bin whose exclusions exceed a search limit is retained, and a cross bin whose selection +// exceeds one is ignored, each with a runtime warning; sampling still applies the exclusions. +module t ( + input clk +); + int cyc = 0; + +`ifdef LIMIT_DEPTH + localparam bit [1024:0] VALUE = 1; + localparam real EXCLUDED_COVERAGE = 100.0 * 1.0 / 3.0; + covergroup cg with function sample (bit [1024:0] value, bit side); + cp: coverpoint value { + bins whole = {[$ : $]}; + ignore_bins endpoints = {1025'b0, {1025{1'b1}}}; + } + other: coverpoint side { + bins zero = {0}; + } + cx: cross cp, other{bins selected = binsof (cp) intersect {[0 : $]};} + endgroup +`else + localparam logic [63:0] ANY = 64'bx; + localparam bit [63:0] VALUE = 64'h0000_0000_00ff_ffff; +`ifdef LIMIT_QUERY + localparam real EXCLUDED_COVERAGE = 100.0 * 1.0 / 3.0; +`else + localparam real EXCLUDED_COVERAGE = 50.0; +`endif + covergroup cg with function sample (bit [63:0] value, bit side); + cp: coverpoint value { + bins whole = {[$ : $]}; + wildcard ignore_bins pairs = { + ANY & ~64'h0000000100000001, ANY & ~64'h0000000200000002, + ANY & ~64'h0000000400000004, ANY & ~64'h0000000800000008, + ANY & ~64'h0000001000000010, ANY & ~64'h0000002000000020, + ANY & ~64'h0000004000000040, ANY & ~64'h0000008000000080, + ANY & ~64'h0000010000000100, ANY & ~64'h0000020000000200, + ANY & ~64'h0000040000000400, ANY & ~64'h0000080000000800, + ANY & ~64'h0000100000001000, ANY & ~64'h0000200000002000, + ANY & ~64'h0000400000004000, ANY & ~64'h0000800000008000, + ANY & ~64'h0001000000010000, ANY & ~64'h0002000000020000, + ANY & ~64'h0004000000040000, ANY & ~64'h0008000000080000, + ANY & ~64'h0010000000100000, ANY & ~64'h0020000000200000, + ANY & ~64'h0040000000400000, ANY & ~64'h0080000000800000 + }; +`ifndef LIMIT_QUERY + ignore_bins endpoint = {64'hffffffffffffffff}; +`endif + } + other: coverpoint side { + bins zero = {0}; + } +`ifdef LIMIT_QUERY + cx: cross cp, other{bins selected = binsof (cp) intersect {[0 : 64'hfffffffefffffffe]};} +`endif + endgroup +`endif + + cg cov; + + always @(posedge clk) begin + ++cyc; + if (cyc == 3) cov = new; + if (cyc == 4) begin + cov.sample(0, 0); // Excluded + `checkr(cov.get_inst_coverage(), EXCLUDED_COVERAGE); + cov.sample(VALUE, 0); + `checkr(cov.get_inst_coverage(), 100.0); + $write("*-* All Finished *-*\n"); + $finish; + end + end +endmodule diff --git a/test_regress/t/t_covergroup_limits_depth.out b/test_regress/t/t_covergroup_limits_depth.out new file mode 100644 index 000000000..deae415d1 --- /dev/null +++ b/test_regress/t/t_covergroup_limits_depth.out @@ -0,0 +1,3 @@ +%Warning: t/t_covergroup_limits.v:24: Coverage bin exclusions exceed the decision-graph depth limit; bin retained +%Warning: t/t_covergroup_limits.v:30: Cross bin selection exceeds the decision-graph depth limit; bin ignored +*-* All Finished *-* diff --git a/test_regress/t/t_covergroup_limits_depth.py b/test_regress/t/t_covergroup_limits_depth.py new file mode 100755 index 000000000..7bd935c03 --- /dev/null +++ b/test_regress/t/t_covergroup_limits_depth.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt_all') + +test.top_filename = 't/t_covergroup_limits.v' + +test.compile(verilator_flags2=['--timing', '+define+LIMIT_DEPTH'], + threads=(2 if test.vltmt else 1)) + +test.execute(expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_covergroup_limits_query.out b/test_regress/t/t_covergroup_limits_query.out new file mode 100644 index 000000000..94768c375 --- /dev/null +++ b/test_regress/t/t_covergroup_limits_query.out @@ -0,0 +1,2 @@ +%Warning: t/t_covergroup_limits.v:65: Cross bin selection exceeds the decision-graph work limit; bin ignored +*-* All Finished *-* diff --git a/test_regress/t/t_covergroup_limits_query.py b/test_regress/t/t_covergroup_limits_query.py new file mode 100755 index 000000000..9701b5afe --- /dev/null +++ b/test_regress/t/t_covergroup_limits_query.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt_all') + +test.top_filename = 't/t_covergroup_limits.v' + +test.compile(verilator_flags2=['--timing', '+define+LIMIT_QUERY'], + threads=(2 if test.vltmt else 1)) + +test.execute(expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_covergroup_wild_real_bad.out b/test_regress/t/t_covergroup_wild_real_bad.out new file mode 100644 index 000000000..560aee807 --- /dev/null +++ b/test_regress/t/t_covergroup_wild_real_bad.out @@ -0,0 +1,23 @@ +%Error: t/t_covergroup_wild_real_bad.v:11:9: Cannot use a wildcard bin on a coverpoint of type 'real' (IEEE 1800-2023 19.5.4). + : ... note: In instance 't' + 11 | cp: coverpoint value { + | ^~~~~~~~~~ + t/t_covergroup_wild_real_bad.v:12:21: ... Location of wildcard bin + 12 | wildcard bins one = {1}; + | ^~~ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Error: t/t_covergroup_wild_real_bad.v:14:15: Cannot use a wildcard bin on a coverpoint of type 'real' (IEEE 1800-2023 19.5.4). + : ... note: In instance 't' + 14 | cp_range: coverpoint value { + | ^~~~~~~~~~ + t/t_covergroup_wild_real_bad.v:15:21: ... Location of wildcard bin + 15 | wildcard bins range = {[1 : 2]}; + | ^~~~~ +%Error: t/t_covergroup_wild_real_bad.v:17:14: Cannot use a wildcard bin on a coverpoint of type 'real' (IEEE 1800-2023 19.5.4). + : ... note: In instance 't' + 17 | cp_open: coverpoint value { + | ^~~~~~~~~~ + t/t_covergroup_wild_real_bad.v:18:21: ... Location of wildcard bin + 18 | wildcard bins open = {[1 : $]}; + | ^~~~ +%Error: Exiting due to diff --git a/test_regress/t/t_covergroup_wild_real_bad.py b/test_regress/t/t_covergroup_wild_real_bad.py new file mode 100755 index 000000000..38cf36b43 --- /dev/null +++ b/test_regress/t/t_covergroup_wild_real_bad.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('linter') + +test.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_covergroup_wild_real_bad.v b/test_regress/t/t_covergroup_wild_real_bad.v new file mode 100644 index 000000000..ff8e86cb6 --- /dev/null +++ b/test_regress/t/t_covergroup_wild_real_bad.v @@ -0,0 +1,25 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t; + real value; + + covergroup cg; + cp: coverpoint value { + wildcard bins one = {1}; + } + cp_range: coverpoint value { + wildcard bins range = {[1 : 2]}; + } + cp_open: coverpoint value { + wildcard bins open = {[1 : $]}; + } + endgroup + + cg cov = new; + + initial $finish; +endmodule diff --git a/test_regress/t/t_debug_emitv.out b/test_regress/t/t_debug_emitv.out index 5ff5788fc..6f09c59da 100644 --- a/test_regress/t/t_debug_emitv.out +++ b/test_regress/t/t_debug_emitv.out @@ -1023,6 +1023,59 @@ module Vt_debug_emitv_t; input string name; endfunction endcovergroup + covergroup Vt_debug_emitv_cg_live_cross; + function new; + cp_x: coverpoint cg_sig { + ignore_bins removed = {'sh0}; + }; + cp_y: coverpoint cg_sig2; + cx: cross cp_x, cp_y { + bins all = cx; + } + endfunction + int signed __Vint; + struct { + string name; + int signed weight; + int signed goal; + string comment; + int signed at_least; + int signed auto_bin_max; + int signed cross_num_print_missing; + bit cross_retain_auto_bins; + bit detect_overlap; + bit per_instance; + bit get_inst_coverage; + } option; + struct { + int signed weight; + int signed goal; + string comment; + bit strobe; + bit merge_instances; + bit distribute_first; + real real_interval; + } type_option; + function sample; + endfunction + function start; + endfunction + function stop; + endfunction + function get_coverage; + get_coverage = /*CRESET*/; + input string covered_bins; + input string total_bins; + endfunction + function get_inst_coverage; + get_inst_coverage = /*CRESET*/; + input string covered_bins; + input string total_bins; + endfunction + function set_inst_name; + input string name; + endfunction + endcovergroup Vt_debug_emitv_cg_basic cg_basic_instVt_debug_emitv_cg_basic; cg_basic_inst = new(); Vt_debug_emitv_cg_clocked cg_clocked_instVt_debug_emitv_cg_clocked; @@ -1031,6 +1084,8 @@ module Vt_debug_emitv_t; cg_trans_inst = new(); Vt_debug_emitv_cg_cross cg_cross_instVt_debug_emitv_cg_cross; cg_cross_inst = new(); + Vt_debug_emitv_cg_live_cross cg_live_cross_instVt_debug_emitv_cg_live_cross; + cg_live_cross_inst = new(); enum logic [2:0] { ZERO = 3'h0, ONE = 3'h1 diff --git a/test_regress/t/t_debug_emitv.v b/test_regress/t/t_debug_emitv.v index f153e3e22..adf34a968 100644 --- a/test_regress/t/t_debug_emitv.v +++ b/test_regress/t/t_debug_emitv.v @@ -452,10 +452,22 @@ module t (/*AUTOARG*/ } endgroup + covergroup cg_live_cross; + option.auto_bin_max = 4; + cp_x: coverpoint cg_sig { + ignore_bins removed = {0}; + } + cp_y: coverpoint cg_sig2; + cx: cross cp_x, cp_y{ + bins all = cx; + } + endgroup + cg_basic cg_basic_inst = new; cg_clocked cg_clocked_inst = new; cg_trans cg_trans_inst = new; cg_cross cg_cross_inst = new; + cg_live_cross cg_live_cross_inst = new; endmodule module sub(input logic clk); diff --git a/test_regress/t/t_vlcov_covergroup.annotate.out b/test_regress/t/t_vlcov_covergroup.annotate.out index be1774562..1d2bb0bb5 100644 --- a/test_regress/t/t_vlcov_covergroup.annotate.out +++ b/test_regress/t/t_vlcov_covergroup.annotate.out @@ -570,18 +570,14 @@ // Crossed coverpoint with a four-state literal in a non-wildcard array bin // (bins av[] = {2'b0x}): LRM 1800-2023 19.5.4 permits 4-state values in a bin definition. // The hit-list sizing cannot statically analyze a 4-state value, so it falls back to the - // safe slot count. Under Verilator's 2-state simulation the value matches addr=0. + // safe slot count. A value with x or z bits does not participate (IEEE 1800-2023 19.5.7), + // so av[0] and its cross bins have no values and leave the coverage computation. covergroup cg_arr_4state; -%000002 cp_addr: coverpoint addr {bins av[] = {2'b0x};} --000002 point: type=covergroup comment= hier=cg_arr_4state.cp_addr.av[0] + cp_addr: coverpoint addr {bins av[] = {2'b0x};} %000001 cp_cmd: coverpoint cmd {bins read = {0}; bins write = {1};} -000001 point: type=covergroup comment= hier=cg_arr_4state.cp_cmd.read -000001 point: type=covergroup comment= hier=cg_arr_4state.cp_cmd.write -%000001 a4: cross cp_addr, cp_cmd; --000001 point: type=covergroup comment= hier=cg_arr_4state.a4.av[0]_x_read - // cross: [av[0], read] --000001 point: type=covergroup comment= hier=cg_arr_4state.a4.av[0]_x_write - // cross: [av[0], write] + a4: cross cp_addr, cp_cmd; endgroup // Crossed coverpoint with two *overlapping* Normal range bins (addr=1 is in both lo and @@ -646,20 +642,16 @@ // cross: [lo, write] endgroup - // Crossed coverpoint with an inverted range bin (lo bound > hi bound): the bin matches no - // value, so the hit-list sizing rejects it (lo > hi) and falls back to the safe slot count. - // The 'inv' bin and its cross bins are therefore never hit (coverage stays at 40%). + // Crossed coverpoint with an inverted range bin (lo bound > hi bound): the range is empty + // (IEEE 1800-2023 11.4.13), so the hit-list sizing rejects it (lo > hi) and falls back to the + // safe slot count. A bin without values, and its cross bins, leave the coverage computation + // (IEEE 1800-2023 19.11.1). covergroup cg_inv; -%000000 cp_addr: coverpoint addr {bins inv = {[3 : 0]};} // inverted -> never matches --000000 point: type=covergroup comment= hier=cg_inv.cp_addr.inv + cp_addr: coverpoint addr {bins inv = {[3 : 0]};} // inverted -> never matches %000001 cp_cmd: coverpoint cmd {bins read = {0}; bins write = {1};} -000001 point: type=covergroup comment= hier=cg_inv.cp_cmd.read -000001 point: type=covergroup comment= hier=cg_inv.cp_cmd.write -%000000 iv: cross cp_addr, cp_cmd; --000000 point: type=covergroup comment= hier=cg_inv.iv.inv_x_read - // cross: [inv, read] --000000 point: type=covergroup comment= hier=cg_inv.iv.inv_x_write - // cross: [inv, write] + iv: cross cp_addr, cp_cmd; endgroup // Crossed coverpoint with *no* Normal bins (only ignore_bins): the cross has an empty bin @@ -1214,13 +1206,13 @@ -000000 point: type=line comment=block hier=top.t -000001 point: type=line comment=else hier=top.t - // Sample cg_arr_4state: 4-state literal bin {2'b0x} matches addr=0 (2-state sim); cross 1x2 - // cg_arr_4state: 1+2+2=5 bins; sample both cmd values -> 100% + // Sample cg_arr_4state: 4-state literal bin {2'b0x} has no value, so addr=0 hits no bin + // cg_arr_4state: 0+2+0=2 bins; sample both cmd values -> 100% %000001 addr = 0; cmd = 0; cg_arr_4state_inst.sample(); -000001 point: type=line comment=block hier=top.t %000001 addr = 0; cmd = 1; cg_arr_4state_inst.sample(); -000001 point: type=line comment=block hier=top.t -%000001 `checkr(cg_arr_4state_inst.get_inst_coverage(), 100.0); // 5/5 +%000001 `checkr(cg_arr_4state_inst.get_inst_coverage(), 100.0); // 2/2 -000001 point: type=line comment=block hier=top.t -000000 point: type=line comment=block hier=top.t -000001 point: type=line comment=else hier=top.t @@ -1272,13 +1264,13 @@ -000000 point: type=line comment=block hier=top.t -000001 point: type=line comment=else hier=top.t - // Sample cg_inv: inverted range bin never matches; only cmd bins are hittable - // cg_inv: 1+2+2=5 bins; inv and its 2 cross bins never hit -> 2/5=40% + // Sample cg_inv: the empty inverted range bin and its cross bins are not counted + // cg_inv: 0+2+0=2 bins -> read + write give 2/2=100% %000001 addr = 0; cmd = 0; cg_inv_inst.sample(); // read -000001 point: type=line comment=block hier=top.t %000001 addr = 1; cmd = 1; cg_inv_inst.sample(); // write -000001 point: type=line comment=block hier=top.t -%000001 `checkr(cg_inv_inst.get_inst_coverage(), 40.0); // 2/5: read + write only +%000001 `checkr(cg_inv_inst.get_inst_coverage(), 100.0); // 2/2: read + write only -000001 point: type=line comment=block hier=top.t -000000 point: type=line comment=block hier=top.t -000001 point: type=line comment=else hier=top.t