2012-04-13 03:08:20 +02:00
|
|
|
// -*- mode: C++; c-file-style: "cc-mode" -*-
|
2006-08-26 13:35:28 +02:00
|
|
|
//*************************************************************************
|
|
|
|
|
// DESCRIPTION: Verilator: Break case statements up and add Unknown assigns
|
|
|
|
|
//
|
2019-11-08 04:33:59 +01:00
|
|
|
// Code available from: https://verilator.org
|
2006-08-26 13:35:28 +02:00
|
|
|
//
|
|
|
|
|
//*************************************************************************
|
|
|
|
|
//
|
2026-01-27 02:24:34 +01:00
|
|
|
// 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: 2003-2026 Wilson Snyder
|
2020-03-21 16:24:24 +01:00
|
|
|
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
|
2006-08-26 13:35:28 +02:00
|
|
|
//
|
|
|
|
|
//*************************************************************************
|
|
|
|
|
// V3Case's Transformations:
|
2008-06-10 03:25:10 +02:00
|
|
|
//
|
2006-08-26 13:35:28 +02:00
|
|
|
// Each module:
|
2019-05-19 22:13:13 +02:00
|
|
|
// TBD: Eliminate tristates by adding __in, __inen, __en wires in parallel
|
|
|
|
|
// Need __en in changed list if a signal is on the LHS of a assign
|
|
|
|
|
// Cases:
|
|
|
|
|
// CASE(v) CASEITEM(items,body) -> IF (OR (EQ (AND v mask)
|
|
|
|
|
// (AND item1 mask))
|
|
|
|
|
// (other items))
|
|
|
|
|
// body
|
|
|
|
|
// Or, converts to a if/else tree.
|
|
|
|
|
// FUTURES:
|
|
|
|
|
// Large 16+ bit tables with constants and no masking (address muxes)
|
2018-02-02 03:24:41 +01:00
|
|
|
// Enter all into std::multimap, sort by value and use a tree of < and == compares.
|
2019-05-19 22:13:13 +02:00
|
|
|
// "Diagonal" find of {rightmost,leftmost} bit {set,clear}
|
2018-02-02 03:24:41 +01:00
|
|
|
// Ignoring mask, check each value is unique (using std::multimap as above?)
|
2019-05-19 22:13:13 +02:00
|
|
|
// Each branch is then mask-and-compare operation (IE
|
|
|
|
|
// <000000001_000000000 at midpoint.)
|
2006-08-26 13:35:28 +02:00
|
|
|
//
|
|
|
|
|
//*************************************************************************
|
2019-10-05 02:17:11 +02:00
|
|
|
|
2023-10-18 12:37:46 +02:00
|
|
|
#include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT
|
|
|
|
|
|
2006-08-26 13:35:28 +02:00
|
|
|
#include "V3Case.h"
|
2022-08-05 11:56:57 +02:00
|
|
|
|
2006-08-26 13:35:28 +02:00
|
|
|
#include "V3Stats.h"
|
|
|
|
|
|
2022-09-18 21:53:42 +02:00
|
|
|
VL_DEFINE_DEBUG_FUNCTIONS;
|
|
|
|
|
|
2006-08-26 13:35:28 +02:00
|
|
|
//######################################################################
|
|
|
|
|
|
2023-03-18 17:17:25 +01:00
|
|
|
class CaseLintVisitor final : public VNVisitorConst {
|
2025-09-23 20:49:01 +02:00
|
|
|
// Under a CASE value node, if so the relevant case statement
|
|
|
|
|
const AstNode* m_casep = nullptr;
|
2009-01-21 22:56:50 +01:00
|
|
|
|
2018-05-14 12:50:47 +02:00
|
|
|
// METHODS
|
2025-09-23 20:49:01 +02:00
|
|
|
template <typename CaseItem>
|
|
|
|
|
static void detectMultipleDefaults(CaseItem* itemsp) {
|
2019-05-19 22:13:13 +02:00
|
|
|
bool hitDefault = false;
|
2025-09-23 20:49:01 +02:00
|
|
|
for (CaseItem* itemp = itemsp; itemp; itemp = AstNode::as<CaseItem>(itemp->nextp())) {
|
|
|
|
|
if (!itemp->isDefault()) continue;
|
|
|
|
|
if (hitDefault) itemp->v3error("Multiple default statements in case statement.");
|
|
|
|
|
hitDefault = true;
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2025-09-23 20:49:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <typename CaseItem>
|
|
|
|
|
void checkXZinNonCaseX(AstNode* casep, AstNodeExpr* exprp, CaseItem* itemsp) {
|
|
|
|
|
VL_RESTORER(m_casep);
|
|
|
|
|
m_casep = casep;
|
|
|
|
|
iterateConst(exprp);
|
|
|
|
|
for (CaseItem* itemp = itemsp; itemp; itemp = AstNode::as<CaseItem>(itemp->nextp())) {
|
|
|
|
|
iterateAndNextConstNull(itemp->condsp());
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-05-19 22:13:13 +02:00
|
|
|
|
2025-09-23 20:49:01 +02:00
|
|
|
// VISITORS
|
|
|
|
|
void visit(AstGenCase* nodep) override {
|
|
|
|
|
// Detect multiple defaults
|
|
|
|
|
detectMultipleDefaults(nodep->itemsp());
|
2019-05-19 22:13:13 +02:00
|
|
|
// Check for X/Z in non-casex statements
|
2025-09-23 20:49:01 +02:00
|
|
|
checkXZinNonCaseX(nodep, nodep->exprp(), nodep->itemsp());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void visit(AstCase* nodep) override {
|
|
|
|
|
if (nodep->casex()) {
|
|
|
|
|
nodep->v3warn(CASEX, "Suggest casez (with ?'s) in place of casex (with X's)");
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2025-09-23 20:49:01 +02:00
|
|
|
// Detect multiple defaults
|
|
|
|
|
detectMultipleDefaults(nodep->itemsp());
|
|
|
|
|
// Check for X/Z in non-casex statements
|
|
|
|
|
checkXZinNonCaseX(nodep, nodep->exprp(), nodep->itemsp());
|
2006-08-26 13:35:28 +02:00
|
|
|
}
|
2022-09-16 12:22:11 +02:00
|
|
|
void visit(AstConst* nodep) override {
|
2025-09-23 20:49:01 +02:00
|
|
|
if (!nodep->num().isFourState()) return;
|
|
|
|
|
|
|
|
|
|
// Error if generate case
|
|
|
|
|
if (VN_IS(m_casep, GenCase)) {
|
|
|
|
|
nodep->v3error("Use of x/? constant in generate case statement, "
|
|
|
|
|
"(no such thing as 'generate casez')");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Otherwise must be a case statement
|
|
|
|
|
const AstCase* const casep = VN_AS(m_casep, Case);
|
|
|
|
|
|
|
|
|
|
// Don't sweat it, we already complained about casex in general
|
|
|
|
|
if (casep->casex()) return;
|
|
|
|
|
|
|
|
|
|
if (casep->casez() || casep->caseInside()) {
|
|
|
|
|
if (nodep->num().isAnyX()) {
|
|
|
|
|
nodep->v3warn(CASEWITHX, "Use of x constant in casez statement, "
|
|
|
|
|
"(perhaps intended ?/z in constant)");
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2025-09-23 20:49:01 +02:00
|
|
|
return;
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2025-09-23 20:49:01 +02:00
|
|
|
|
|
|
|
|
nodep->v3warn(CASEWITHX, "Use of x/? constant in case statement, "
|
|
|
|
|
"(perhaps intended casex/casez)");
|
2006-08-26 13:35:28 +02:00
|
|
|
}
|
2023-03-18 17:17:25 +01:00
|
|
|
void visit(AstNode* nodep) override { iterateChildrenConst(nodep); }
|
2020-04-04 14:31:14 +02:00
|
|
|
|
2019-09-12 13:22:22 +02:00
|
|
|
// CONSTRUCTORS
|
2025-09-23 20:49:01 +02:00
|
|
|
explicit CaseLintVisitor(AstCase* nodep) { iterateConst(nodep); }
|
|
|
|
|
explicit CaseLintVisitor(AstGenCase* nodep) { iterateConst(nodep); }
|
2022-09-16 12:22:11 +02:00
|
|
|
~CaseLintVisitor() override = default;
|
2026-06-12 15:15:41 +02:00
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
static void apply(AstCase* nodep) { CaseLintVisitor{nodep}; }
|
|
|
|
|
static void apply(AstGenCase* nodep) { CaseLintVisitor{nodep}; }
|
2006-08-26 13:35:28 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
//######################################################################
|
|
|
|
|
// Case state, as a visitor of each AstNode
|
|
|
|
|
|
2022-01-02 19:56:40 +01:00
|
|
|
class CaseVisitor final : public VNVisitor {
|
2026-06-12 15:15:41 +02:00
|
|
|
// Maximum width we can check for overlaps/exhaustiveness
|
|
|
|
|
constexpr static int CASE_OVERLAP_WIDTH = 16;
|
|
|
|
|
// Maximum number of case values for exhaustive analysis/optimization
|
|
|
|
|
constexpr static int CASE_MAX_VALUES = 1 << CASE_OVERLAP_WIDTH;
|
|
|
|
|
// Levels of priority to be ORed together in top IF tree
|
|
|
|
|
constexpr static int CASE_ENCODER_GROUP_DEPTH = 8;
|
|
|
|
|
|
|
|
|
|
// TYPES
|
|
|
|
|
// Record for each case value
|
|
|
|
|
struct CaseRecord final {
|
|
|
|
|
AstCaseItem* itemp; // Case item that covers value
|
|
|
|
|
AstConst* constp; // Expression within 'itemp' that covers value (nullptr for default)
|
|
|
|
|
AstNode* stmtsp; // Statements of 'itemp' (might be nullptr if case is empty)
|
|
|
|
|
};
|
2006-08-26 13:35:28 +02:00
|
|
|
|
|
|
|
|
// STATE
|
2019-10-05 13:54:14 +02:00
|
|
|
VDouble0 m_statCaseFast; // Statistic tracking
|
|
|
|
|
VDouble0 m_statCaseSlow; // Statistic tracking
|
2021-11-26 23:55:36 +01:00
|
|
|
const AstNode* m_alwaysp = nullptr; // Always in which case is located
|
2006-08-26 13:35:28 +02:00
|
|
|
|
|
|
|
|
// Per-CASE
|
2026-06-12 15:15:41 +02:00
|
|
|
bool m_caseExhaustive = false; // Proven exhaustive
|
|
|
|
|
bool m_caseNoOverlaps = false; // Proven no overlaps between cases
|
|
|
|
|
// Map from value (index) to the CaseRecord that covers this value
|
|
|
|
|
std::array<CaseRecord, CASE_MAX_VALUES> m_value2CaseRecord;
|
2006-08-26 13:35:28 +02:00
|
|
|
|
|
|
|
|
// METHODS
|
2026-06-12 15:15:41 +02:00
|
|
|
// Determine whether we should check case items are complete
|
|
|
|
|
// Returns enum's dtype if should check, nullptr if shouldn't
|
|
|
|
|
static const AstEnumDType* getEnumCompletionCheckDType(const AstCase* const nodep) {
|
2023-09-14 13:22:49 +02:00
|
|
|
if (!nodep->uniquePragma() && !nodep->unique0Pragma()) return nullptr;
|
|
|
|
|
const AstEnumDType* const enumDtp
|
2022-12-01 01:42:21 +01:00
|
|
|
= VN_CAST(nodep->exprp()->dtypep()->skipRefToEnump(), EnumDType);
|
2023-09-14 13:22:49 +02:00
|
|
|
if (!enumDtp) return nullptr; // Case isn't enum
|
|
|
|
|
const AstBasicDType* const basicp = enumDtp->subDTypep()->basicp();
|
|
|
|
|
if (!basicp) return nullptr; // Not simple type (perhaps IEEE illegal)
|
|
|
|
|
return enumDtp;
|
|
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
|
|
|
|
|
// Check and warn if case items are not complete over the given enum type.
|
|
|
|
|
// Returns true iff the case items cover all enum values/patterns.
|
|
|
|
|
bool checkExhaustiveEnum(const AstCase* const nodep, const AstEnumDType* const enump) {
|
|
|
|
|
const uint32_t numCases = 1UL << nodep->exprp()->width();
|
|
|
|
|
for (AstEnumItem* eip = enump->itemsp(); eip; eip = VN_AS(eip->nextp(), EnumItem)) {
|
|
|
|
|
AstConst* const econstp = VN_AS(eip->valuep(), Const);
|
|
|
|
|
V3Number nummask{eip, econstp->width()};
|
2023-09-14 13:22:49 +02:00
|
|
|
nummask.opBitsNonX(econstp->num());
|
2026-06-12 15:15:41 +02:00
|
|
|
V3Number numval{eip, econstp->width()};
|
2023-09-14 13:22:49 +02:00
|
|
|
numval.opBitsOne(econstp->num());
|
2026-06-12 15:15:41 +02:00
|
|
|
|
|
|
|
|
const uint32_t mask = nummask.toUInt();
|
2023-09-14 13:22:49 +02:00
|
|
|
const uint32_t val = numval.toUInt();
|
|
|
|
|
|
2026-06-12 15:15:41 +02:00
|
|
|
// Check all cases to see if they cover this enum value/pattern
|
2023-09-14 13:22:49 +02:00
|
|
|
for (uint32_t i = 0; i < numCases; ++i) {
|
2026-06-12 15:15:41 +02:00
|
|
|
if ((i & mask) != val) continue; // This case is not for this enum value
|
|
|
|
|
if (m_value2CaseRecord[i].itemp) continue; // Covered case
|
|
|
|
|
// Warn unless unique0 case which allows no-match
|
|
|
|
|
if (!nodep->unique0Pragma()) {
|
|
|
|
|
nodep->v3warn(CASEINCOMPLETE,
|
|
|
|
|
"Enum item " << eip->prettyNameQ() << " not covered by case");
|
2023-09-14 13:22:49 +02:00
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
// TODO: warn for all uncovered enum values, not just the first
|
|
|
|
|
return false; // enum has uncovered value by case items
|
2023-09-14 13:22:49 +02:00
|
|
|
}
|
2022-12-01 01:42:21 +01:00
|
|
|
}
|
2023-09-14 13:22:49 +02:00
|
|
|
return true; // enum is fully covered
|
2022-12-01 01:42:21 +01:00
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
|
|
|
|
|
// Check and warn if case items are not complete over all possible values.
|
|
|
|
|
// Returns true iff the case items cover all values of the case expression.
|
|
|
|
|
bool checkExhaustivePacked(AstCase* nodep) {
|
|
|
|
|
const uint32_t numCases = 1UL << nodep->exprp()->width();
|
|
|
|
|
for (uint32_t i = 0; i < numCases; ++i) {
|
|
|
|
|
if (m_value2CaseRecord[i].itemp) continue; // Covered case
|
|
|
|
|
if (!nodep->unique0Pragma()) {
|
|
|
|
|
nodep->v3warn(CASEINCOMPLETE,
|
|
|
|
|
"Case values incompletely covered (example pattern 0x" << std::hex
|
|
|
|
|
<< i << ")");
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
// TODO: warn for more than one uncovered case, not just the first
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// It's an exhaustive case statement
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool checkExhaustive(AstCase* nodep) {
|
|
|
|
|
if (const AstEnumDType* const enump = getEnumCompletionCheckDType(nodep)) {
|
|
|
|
|
return checkExhaustiveEnum(nodep, enump);
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
return checkExhaustivePacked(nodep);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isCaseTreeFast(AstCase* nodep) {
|
|
|
|
|
m_caseExhaustive = true; // TODO: we haven't proven this yet, but is as was before
|
|
|
|
|
m_caseNoOverlaps = false;
|
|
|
|
|
|
|
|
|
|
AstNode* const caseExprp = nodep->exprp();
|
|
|
|
|
if (caseExprp->isDouble() || caseExprp->isString()) return false;
|
|
|
|
|
|
|
|
|
|
const int caseWidth = caseExprp->width();
|
|
|
|
|
if (!caseWidth) return false;
|
|
|
|
|
if (caseWidth > CASE_OVERLAP_WIDTH) return false;
|
|
|
|
|
|
|
|
|
|
int caseConditions = 0;
|
|
|
|
|
|
|
|
|
|
for (AstCaseItem* cip = nodep->itemsp(); cip; cip = VN_AS(cip->nextp(), CaseItem)) {
|
|
|
|
|
for (AstNode* condp = cip->condsp(); condp; condp = condp->nextp()) {
|
|
|
|
|
// Can't do anything with non-constants
|
|
|
|
|
if (!VN_IS(condp, Const)) return false;
|
|
|
|
|
// Count conditions
|
|
|
|
|
++caseConditions;
|
|
|
|
|
}
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
|
2025-05-23 02:29:32 +02:00
|
|
|
UINFO(8, "Simple case statement: " << nodep);
|
2026-06-12 15:15:41 +02:00
|
|
|
const uint32_t numCases = 1UL << caseWidth;
|
2019-05-19 22:13:13 +02:00
|
|
|
// Zero list of items for each value
|
2026-06-12 15:15:41 +02:00
|
|
|
for (uint32_t i = 0; i < numCases; ++i) {
|
|
|
|
|
m_value2CaseRecord[i].itemp = nullptr;
|
|
|
|
|
m_value2CaseRecord[i].constp = nullptr;
|
|
|
|
|
m_value2CaseRecord[i].stmtsp = nullptr;
|
|
|
|
|
}
|
2019-05-19 22:13:13 +02:00
|
|
|
// Now pick up the values for each assignment
|
|
|
|
|
// We can cheat and use uint32_t's because we only support narrow case's
|
2021-03-29 01:57:36 +02:00
|
|
|
bool reportedOverlap = false;
|
|
|
|
|
bool reportedSubcase = false;
|
2026-06-12 15:15:41 +02:00
|
|
|
bool hasDefault = false;
|
|
|
|
|
m_caseNoOverlaps = true;
|
2020-04-15 13:58:34 +02:00
|
|
|
for (AstCaseItem* itemp = nodep->itemsp(); itemp;
|
2021-10-22 14:56:48 +02:00
|
|
|
itemp = VN_AS(itemp->nextp(), CaseItem)) {
|
2026-06-12 15:15:41 +02:00
|
|
|
|
|
|
|
|
// Default case
|
|
|
|
|
if (itemp->isDefault()) {
|
|
|
|
|
// Default was moved to be the last item by V3LinkDot. Fill remaining cases
|
|
|
|
|
for (uint32_t i = 0; i < numCases; ++i) {
|
|
|
|
|
CaseRecord& caseRecord = m_value2CaseRecord[i];
|
|
|
|
|
if (!caseRecord.itemp) {
|
|
|
|
|
caseRecord.itemp = itemp;
|
|
|
|
|
caseRecord.stmtsp = itemp->stmtsp();
|
2021-03-29 01:57:36 +02:00
|
|
|
}
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
hasDefault = true;
|
|
|
|
|
continue;
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
|
|
|
|
|
for (AstConst* iconstp = VN_AS(itemp->condsp(), Const); iconstp;
|
|
|
|
|
iconstp = VN_AS(iconstp->nextp(), Const)) {
|
|
|
|
|
// Some items can never match due to 2-state simulation
|
|
|
|
|
if (neverItem(nodep, iconstp)) continue;
|
|
|
|
|
|
|
|
|
|
V3Number nummask{itemp, iconstp->width()};
|
|
|
|
|
nummask.opBitsNonX(iconstp->num());
|
|
|
|
|
V3Number numval{itemp, iconstp->width()};
|
|
|
|
|
numval.opBitsOne(iconstp->num());
|
|
|
|
|
|
|
|
|
|
const uint32_t mask = nummask.toUInt();
|
|
|
|
|
const uint32_t val = numval.toUInt();
|
|
|
|
|
|
2026-06-12 13:22:18 +02:00
|
|
|
bool foundNewCase = false;
|
|
|
|
|
const AstConst* firstOverlapConstp = nullptr;
|
|
|
|
|
uint32_t firstOverlapValue = 0;
|
2021-03-29 01:57:36 +02:00
|
|
|
for (uint32_t i = 0; i < numCases; ++i) {
|
2026-06-12 15:15:41 +02:00
|
|
|
if ((i & mask) != val) continue;
|
|
|
|
|
|
|
|
|
|
CaseRecord& caseRecord = m_value2CaseRecord[i];
|
|
|
|
|
|
|
|
|
|
// If this is the first case that covers this value, record it
|
|
|
|
|
if (!caseRecord.itemp) {
|
|
|
|
|
caseRecord.itemp = itemp;
|
|
|
|
|
caseRecord.constp = iconstp;
|
|
|
|
|
caseRecord.stmtsp = itemp->stmtsp();
|
2026-06-12 13:22:18 +02:00
|
|
|
foundNewCase = true;
|
2026-06-12 15:15:41 +02:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 13:22:18 +02:00
|
|
|
// There is an overlap. If it's within the same CaseItem, it is legal
|
|
|
|
|
if (caseRecord.itemp == itemp) continue;
|
|
|
|
|
|
|
|
|
|
// Otherwise record the first overlapping case
|
|
|
|
|
if (!firstOverlapConstp) {
|
|
|
|
|
firstOverlapConstp = caseRecord.constp;
|
|
|
|
|
firstOverlapValue = i;
|
2026-06-12 15:15:41 +02:00
|
|
|
m_caseNoOverlaps = false;
|
|
|
|
|
}
|
2022-12-01 01:42:21 +01:00
|
|
|
}
|
2026-06-12 13:22:18 +02:00
|
|
|
if (nodep->priorityPragma()) {
|
|
|
|
|
// If this is a priority case, we only want to complain if every possible value
|
|
|
|
|
// for this item is already hit by some other item. This is true if
|
|
|
|
|
// 'foundNewCase' is false. 'firstOverlapConstp' is null when the only covering
|
|
|
|
|
// item is this item itself, which is legal overlap within one item.
|
|
|
|
|
if (!reportedSubcase && !foundNewCase && firstOverlapConstp) {
|
|
|
|
|
iconstp->v3warn(CASEOVERLAP,
|
|
|
|
|
"Case item ignored: every matching value is covered "
|
|
|
|
|
"by an earlier condition\n"
|
|
|
|
|
<< iconstp->warnContextPrimary() << '\n'
|
|
|
|
|
<< firstOverlapConstp->warnOther()
|
|
|
|
|
<< "... Location of previous condition\n"
|
|
|
|
|
<< firstOverlapConstp->warnContextPrimary());
|
|
|
|
|
reportedSubcase = true;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// If this case statement doesn't have the priority keyword,
|
|
|
|
|
// we want to warn on any overlap.
|
|
|
|
|
if (!reportedOverlap && firstOverlapConstp) {
|
2026-06-12 15:15:41 +02:00
|
|
|
std::ostringstream examplePattern;
|
|
|
|
|
if (iconstp->num().isAnyXZ()) {
|
2026-06-12 13:22:18 +02:00
|
|
|
examplePattern << " (example pattern 0x" << std::hex
|
|
|
|
|
<< firstOverlapValue << ")";
|
2026-06-12 15:15:41 +02:00
|
|
|
}
|
|
|
|
|
iconstp->v3warn(CASEOVERLAP,
|
|
|
|
|
"Case conditions overlap"
|
|
|
|
|
<< examplePattern.str() << "\n"
|
|
|
|
|
<< iconstp->warnContextPrimary() << '\n'
|
2026-06-12 13:22:18 +02:00
|
|
|
<< firstOverlapConstp->warnOther()
|
2026-06-12 15:15:41 +02:00
|
|
|
<< "... Location of overlapping condition\n"
|
2026-06-12 13:22:18 +02:00
|
|
|
<< firstOverlapConstp->warnContextSecondary());
|
2026-06-12 15:15:41 +02:00
|
|
|
reportedOverlap = true;
|
|
|
|
|
}
|
2023-09-14 13:22:49 +02:00
|
|
|
}
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
|
|
|
|
}
|
2022-12-01 01:42:21 +01:00
|
|
|
|
2026-06-12 15:15:41 +02:00
|
|
|
// If there was no default, check exhaustiveness
|
|
|
|
|
m_caseExhaustive = hasDefault || checkExhaustive(nodep);
|
|
|
|
|
if (!m_caseExhaustive) {
|
|
|
|
|
m_caseNoOverlaps = false;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (caseConditions <= 3
|
2019-12-23 13:47:57 +01:00
|
|
|
// Avoid e.g. priority expanders from going crazy in expansion
|
2026-06-12 15:15:41 +02:00
|
|
|
|| (caseWidth >= 8 && (caseConditions <= (caseWidth + 1)))) {
|
2019-12-23 13:47:57 +01:00
|
|
|
return false; // Not worth simplifying
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-19 22:13:13 +02:00
|
|
|
return true; // All is fine
|
2006-08-26 13:35:28 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-12 15:15:41 +02:00
|
|
|
// TODO: should return AstNodeStmt after #6280
|
2022-11-13 21:33:11 +01:00
|
|
|
AstNode* replaceCaseFastRecurse(AstNodeExpr* cexprp, int msb, uint32_t upperValue) {
|
2026-06-12 15:15:41 +02:00
|
|
|
// Base case: If reached the last bit, upperValue equals an exact value, just return
|
|
|
|
|
// the statements from that CaseItem. Note: Not cloning here as the caller will do
|
|
|
|
|
// an identity check.
|
|
|
|
|
if (msb < 0) return m_value2CaseRecord[upperValue].stmtsp;
|
|
|
|
|
|
|
|
|
|
// Recursive case:
|
|
|
|
|
// Make left and right subtrees assuming cexpr[msb] is 0 and 1 respectively
|
|
|
|
|
const uint32_t upperValue0 = upperValue;
|
|
|
|
|
const uint32_t upperValue1 = upperValue | (1UL << msb);
|
|
|
|
|
AstNode* tree0p = replaceCaseFastRecurse(cexprp, msb - 1, upperValue0);
|
|
|
|
|
AstNode* tree1p = replaceCaseFastRecurse(cexprp, msb - 1, upperValue1);
|
|
|
|
|
|
|
|
|
|
// If same logic on both sides, we can just return one of them
|
|
|
|
|
if (tree0p == tree1p) return tree0p;
|
|
|
|
|
|
|
|
|
|
// We could have a "checkerboard" with A B A B, we can use the same IF on both edges
|
|
|
|
|
{
|
2019-05-19 22:13:13 +02:00
|
|
|
bool same = true;
|
2026-06-12 15:15:41 +02:00
|
|
|
for (uint32_t a = upperValue0, b = upperValue1; a < upperValue1; ++a, ++b) {
|
|
|
|
|
if (m_value2CaseRecord[a].stmtsp != m_value2CaseRecord[b].stmtsp) {
|
2020-04-15 13:58:34 +02:00
|
|
|
same = false;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
|
|
|
|
if (same) {
|
2020-01-17 02:17:11 +01:00
|
|
|
VL_DO_DANGLING(tree1p->deleteTree(), tree1p);
|
2019-05-19 22:13:13 +02:00
|
|
|
return tree0p;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
|
|
|
|
|
// Must have differing logic. Test the bit and convert to an If.
|
|
|
|
|
|
|
|
|
|
// Clone if needed
|
|
|
|
|
if (tree0p && tree0p->backp()) tree0p = tree0p->cloneTree(true);
|
|
|
|
|
if (tree1p && tree1p->backp()) tree1p = tree1p->cloneTree(true);
|
|
|
|
|
// Create the If statement
|
|
|
|
|
FileLine* const flp = cexprp->fileline();
|
|
|
|
|
AstNodeExpr* const condp = new AstSel{flp, cexprp->cloneTreePure(false), msb, 1};
|
|
|
|
|
AstIf* const ifp = new AstIf{flp, condp, tree1p, tree0p};
|
|
|
|
|
return ifp;
|
2006-08-26 13:35:28 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-12 15:15:41 +02:00
|
|
|
// TODO: should return AstNodeStmt after #6280
|
|
|
|
|
AstNode* replaceCaseFast(AstCase* nodep) {
|
2019-05-19 22:13:13 +02:00
|
|
|
// CASEx(cexpr,....
|
|
|
|
|
// -> tree of IF(msb, IF(msb-1, 11, 10)
|
|
|
|
|
// IF(msb-1, 01, 00))
|
2026-06-12 15:15:41 +02:00
|
|
|
const int caseWidth = nodep->exprp()->width();
|
|
|
|
|
AstNode* const ifrootp = replaceCaseFastRecurse(nodep->exprp(), caseWidth - 1, 0UL);
|
|
|
|
|
return ifrootp && ifrootp->backp() ? ifrootp->cloneTree(true) : ifrootp;
|
2006-08-26 13:35:28 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-12 15:15:41 +02:00
|
|
|
// TODO: should return AstNodeStmt after #6280
|
|
|
|
|
AstNode* replaceCaseComplicated(AstCase* nodep) {
|
2019-05-19 22:13:13 +02:00
|
|
|
// CASEx(cexpr,ITEM(icond1,istmts1),ITEM(icond2,istmts2),ITEM(default,istmts3))
|
|
|
|
|
// -> IF((cexpr==icond1),istmts1,
|
|
|
|
|
// IF((EQ (AND MASK cexpr) (AND MASK icond1)
|
|
|
|
|
// ,istmts2, istmts3
|
2026-06-12 15:15:41 +02:00
|
|
|
|
|
|
|
|
// We'll do this in two stages.
|
|
|
|
|
// First stage, convert the conditions to the appropriate IF AND terms.
|
|
|
|
|
bool hasDefault = false;
|
2020-04-15 13:58:34 +02:00
|
|
|
for (AstCaseItem* itemp = nodep->itemsp(); itemp;
|
2021-10-22 14:56:48 +02:00
|
|
|
itemp = VN_AS(itemp->nextp(), CaseItem)) {
|
2026-06-12 15:15:41 +02:00
|
|
|
|
|
|
|
|
FileLine* const flp = itemp->fileline();
|
|
|
|
|
|
|
|
|
|
// Default clause. Just make true, we'll optimize it away later
|
|
|
|
|
if (itemp->isDefault()) {
|
|
|
|
|
itemp->addCondsp(new AstConst{flp, AstConst::BitTrue{}});
|
|
|
|
|
hasDefault = true;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Regular clause. Construct the condition expression.
|
|
|
|
|
AstNodeExpr* newCondp = nullptr;
|
|
|
|
|
for (AstNodeExpr *itemExprp = itemp->condsp(), *nextp; itemExprp; itemExprp = nextp) {
|
|
|
|
|
nextp = VN_AS(itemExprp->nextp(), NodeExpr);
|
|
|
|
|
itemExprp->unlinkFrBack();
|
|
|
|
|
|
|
|
|
|
// If case never matches, ignore it
|
|
|
|
|
if (neverItem(nodep, itemExprp)) {
|
|
|
|
|
VL_DO_DANGLING(itemExprp->deleteTree(), itemExprp);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compute the term to add to the condition expression
|
|
|
|
|
AstNodeExpr* const termp = [&]() -> AstNodeExpr* {
|
|
|
|
|
// Will need a copy of the caseExpr regardless
|
|
|
|
|
AstNodeExpr* const caseExprp = nodep->exprp()->cloneTreePure(false);
|
|
|
|
|
|
|
|
|
|
// InsideRange: Similar logic in V3Width::visit(AstInside)
|
|
|
|
|
if (AstInsideRange* const itemRangep = VN_CAST(itemExprp, InsideRange)) {
|
|
|
|
|
AstNodeExpr* const resultp = itemRangep->newAndFromInside( //
|
|
|
|
|
caseExprp, //
|
|
|
|
|
itemRangep->lhsp()->unlinkFrBack(),
|
|
|
|
|
itemRangep->rhsp()->unlinkFrBack());
|
|
|
|
|
VL_DO_DANGLING2(itemExprp->deleteTree(), itemExprp, itemRangep);
|
|
|
|
|
return resultp;
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
|
|
|
|
|
// Check if we need to perform a wildcard match, this needs masking
|
|
|
|
|
if (AstConst* const itemConstp = VN_CAST(itemExprp, Const)) {
|
|
|
|
|
// TODO: 4-state will need to fix this
|
|
|
|
|
if (itemConstp->num().isFourState()
|
|
|
|
|
&& (nodep->casex() || nodep->casez() || nodep->caseInside())) {
|
|
|
|
|
// Wildcard match, make 'caseExpr' & 'mask' == 'itemExpr' & 'mask'
|
|
|
|
|
V3Number numMask{itemp, itemConstp->width()};
|
|
|
|
|
numMask.opBitsNonX(itemConstp->num());
|
|
|
|
|
V3Number numOne{itemp, itemConstp->width()};
|
|
|
|
|
numOne.opBitsOne(itemConstp->num());
|
|
|
|
|
V3Number numRhs{itemp, itemConstp->width()};
|
|
|
|
|
numRhs.opAnd(numOne, numMask);
|
|
|
|
|
VL_DO_DANGLING2(itemExprp->deleteTree(), itemExprp, itemConstp);
|
|
|
|
|
return AstEq::newTyped(
|
|
|
|
|
flp, //
|
|
|
|
|
new AstConst{flp, numRhs},
|
|
|
|
|
new AstAnd{flp, caseExprp, new AstConst{flp, numMask}});
|
|
|
|
|
}
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
|
|
|
|
|
// Regular case, use simple equality comparison
|
|
|
|
|
return AstEq::newTyped(flp, caseExprp, itemExprp);
|
|
|
|
|
}();
|
|
|
|
|
|
|
|
|
|
// 'Or' new term with previous terms
|
|
|
|
|
newCondp = newCondp ? new AstLogOr{flp, newCondp, termp} : termp;
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
// Replace expression in tree. Needs to be non-null, so add a constant false if needed
|
|
|
|
|
if (!newCondp) newCondp = new AstConst{flp, AstConst::BitFalse{}};
|
|
|
|
|
itemp->addCondsp(newCondp);
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
|
|
|
|
|
// If there was no default, add a empty one, this greatly simplifies below code
|
|
|
|
|
// and constant propagation will just eliminate it for us later.
|
|
|
|
|
if (!hasDefault) {
|
2022-11-20 23:40:38 +01:00
|
|
|
nodep->addItemsp(new AstCaseItem{
|
|
|
|
|
nodep->fileline(), new AstConst{nodep->fileline(), AstConst::BitTrue{}}, nullptr});
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
|
2019-05-19 22:13:13 +02:00
|
|
|
// Now build the IF statement tree
|
2026-06-12 15:15:41 +02:00
|
|
|
// The tree can be quite huge. Pull every group of 8 out, and make a OR tree.
|
2019-05-19 22:13:13 +02:00
|
|
|
// This reduces the depth for the bottom elements, at the cost of
|
|
|
|
|
// some of the top elements. If we ever have profiling data, we
|
|
|
|
|
// should pull out the most common item from here and instead make
|
|
|
|
|
// it the first IF branch.
|
|
|
|
|
int depth = 0;
|
2025-10-17 15:00:32 +02:00
|
|
|
AstIf* grouprootp = nullptr;
|
2020-08-15 16:12:55 +02:00
|
|
|
AstIf* groupnextp = nullptr;
|
|
|
|
|
AstIf* itemnextp = nullptr;
|
2020-04-15 13:58:34 +02:00
|
|
|
for (AstCaseItem* itemp = nodep->itemsp(); itemp;
|
2021-10-22 14:56:48 +02:00
|
|
|
itemp = VN_AS(itemp->nextp(), CaseItem)) {
|
2026-06-12 15:15:41 +02:00
|
|
|
|
|
|
|
|
// Grab the statements from this item. May be empty.
|
|
|
|
|
AstNode* const istmtsp = itemp->stmtsp();
|
2019-05-19 22:13:13 +02:00
|
|
|
if (istmtsp) istmtsp->unlinkFrBackWithNext();
|
2026-06-12 15:15:41 +02:00
|
|
|
|
2019-05-19 22:13:13 +02:00
|
|
|
// Expressioned clause
|
2022-11-13 21:33:11 +01:00
|
|
|
AstNodeExpr* const ifexprp = itemp->condsp()->unlinkFrBack();
|
2020-04-15 13:58:34 +02:00
|
|
|
{ // Prepare for next group
|
2019-05-19 22:13:13 +02:00
|
|
|
if (++depth > CASE_ENCODER_GROUP_DEPTH) depth = 1;
|
|
|
|
|
if (depth == 1) { // First group or starting new group
|
2020-08-15 16:12:55 +02:00
|
|
|
itemnextp = nullptr;
|
2023-09-17 04:50:54 +02:00
|
|
|
AstIf* const newp = new AstIf{itemp->fileline(), ifexprp->cloneTreePure(true)};
|
2020-04-15 13:58:34 +02:00
|
|
|
if (groupnextp) {
|
|
|
|
|
groupnextp->addElsesp(newp);
|
|
|
|
|
} else {
|
|
|
|
|
grouprootp = newp;
|
|
|
|
|
}
|
2019-05-19 22:13:13 +02:00
|
|
|
groupnextp = newp;
|
|
|
|
|
} else { // Continue group, modify if condition to OR in this new condition
|
2022-11-13 21:33:11 +01:00
|
|
|
AstNodeExpr* const condp = groupnextp->condp()->unlinkFrBack();
|
2020-04-15 13:58:34 +02:00
|
|
|
groupnextp->condp(
|
2023-09-17 04:50:54 +02:00
|
|
|
new AstOr{ifexprp->fileline(), condp, ifexprp->cloneTreePure(true)});
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
|
|
|
|
}
|
2020-04-15 13:58:34 +02:00
|
|
|
{ // Make the new lower IF and attach in the tree
|
2022-11-13 21:33:11 +01:00
|
|
|
AstNodeExpr* itemexprp = ifexprp;
|
2020-04-15 13:58:34 +02:00
|
|
|
VL_DANGLING(ifexprp);
|
|
|
|
|
if (depth == CASE_ENCODER_GROUP_DEPTH) { // End of group - can skip the condition
|
2020-01-17 02:17:11 +01:00
|
|
|
VL_DO_DANGLING(itemexprp->deleteTree(), itemexprp);
|
2022-11-20 23:40:38 +01:00
|
|
|
itemexprp = new AstConst{itemp->fileline(), AstConst::BitTrue{}};
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2022-11-20 23:40:38 +01:00
|
|
|
AstIf* const newp = new AstIf{itemp->fileline(), itemexprp, istmtsp};
|
2020-04-15 13:58:34 +02:00
|
|
|
if (itemnextp) {
|
|
|
|
|
itemnextp->addElsesp(newp);
|
|
|
|
|
} else {
|
2022-09-15 20:43:56 +02:00
|
|
|
groupnextp->addThensp(newp); // First in a new group
|
2020-04-15 13:58:34 +02:00
|
|
|
}
|
2019-05-19 22:13:13 +02:00
|
|
|
itemnextp = newp;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
return grouprootp;
|
2006-08-26 13:35:28 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-12 15:15:41 +02:00
|
|
|
bool neverItem(const AstCase* casep, const AstNodeExpr* itemExprp) {
|
|
|
|
|
const AstConst* const constp = VN_CAST(itemExprp, Const);
|
|
|
|
|
if (!constp) return false;
|
2019-05-19 22:13:13 +02:00
|
|
|
// Xs in case or casez are impossible due to two state simulations
|
2026-06-12 15:15:41 +02:00
|
|
|
if (casep->casex() || casep->caseInside()) return false;
|
|
|
|
|
if (casep->casez()) return constp->num().isAnyX();
|
|
|
|
|
return constp->num().isFourState();
|
2008-07-22 19:07:19 +02:00
|
|
|
}
|
|
|
|
|
|
2006-08-26 13:35:28 +02:00
|
|
|
// VISITORS
|
2022-09-16 12:22:11 +02:00
|
|
|
void visit(AstCase* nodep) override {
|
2026-06-12 07:38:21 +02:00
|
|
|
UASSERT_OBJ(nodep->exprp()->isPure(), nodep,
|
|
|
|
|
"Impure case expression should have been removed by V3LiftExpr");
|
2026-06-12 15:15:41 +02:00
|
|
|
|
|
|
|
|
CaseLintVisitor::apply(nodep);
|
|
|
|
|
|
|
|
|
|
// Convert any children first
|
|
|
|
|
iterateChildren(nodep);
|
|
|
|
|
|
|
|
|
|
// Convert the case statement
|
|
|
|
|
AstNode* replacementp = nullptr;
|
2022-06-04 02:43:16 +02:00
|
|
|
if (isCaseTreeFast(nodep) && v3Global.opt.fCase()) {
|
2019-05-19 22:13:13 +02:00
|
|
|
// It's a simple priority encoder or complete statement
|
|
|
|
|
// we can make a tree of statements to avoid extra comparisons
|
|
|
|
|
++m_statCaseFast;
|
2026-06-12 15:15:41 +02:00
|
|
|
replacementp = replaceCaseFast(nodep);
|
2019-05-19 22:13:13 +02:00
|
|
|
} else {
|
2026-06-12 15:15:41 +02:00
|
|
|
// If a case statement is exhaustive, presume signals involved aren't forming a latch
|
|
|
|
|
// TODO: this is broken, but it is as was before
|
|
|
|
|
if (m_alwaysp && m_caseExhaustive) {
|
2025-09-29 03:26:21 +02:00
|
|
|
m_alwaysp->fileline()->warnOff(V3ErrorCode::LATCH, true);
|
2026-06-12 15:15:41 +02:00
|
|
|
}
|
2019-05-19 22:13:13 +02:00
|
|
|
++m_statCaseSlow;
|
2026-06-12 15:15:41 +02:00
|
|
|
m_caseExhaustive = false;
|
|
|
|
|
m_caseNoOverlaps = false;
|
|
|
|
|
replacementp = replaceCaseComplicated(nodep);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Take the notParallelp tree under the case statement created by V3Assert
|
|
|
|
|
// If the statement was proven to have no overlaps and all cases covered,
|
|
|
|
|
// it can be removed. Otherwise insert the assertion after the case statement.
|
|
|
|
|
if (nodep->notParallelp() && (!m_caseExhaustive || !m_caseNoOverlaps)) {
|
|
|
|
|
nodep->addNextHere(nodep->notParallelp()->unlinkFrBackWithNext());
|
2019-05-19 22:13:13 +02:00
|
|
|
}
|
2026-06-12 15:15:41 +02:00
|
|
|
|
|
|
|
|
// Replace/remove the case statement
|
|
|
|
|
if (replacementp) {
|
|
|
|
|
nodep->replaceWith(replacementp);
|
|
|
|
|
} else {
|
|
|
|
|
nodep->unlinkFrBack();
|
|
|
|
|
}
|
|
|
|
|
VL_DO_DANGLING(nodep->deleteTree(), nodep);
|
2006-08-26 13:35:28 +02:00
|
|
|
}
|
|
|
|
|
//--------------------
|
2023-09-14 13:22:49 +02:00
|
|
|
void visit(AstAlways* nodep) override {
|
2025-04-03 05:13:43 +02:00
|
|
|
VL_RESTORER(m_alwaysp);
|
2023-09-14 13:22:49 +02:00
|
|
|
m_alwaysp = nodep;
|
2021-01-05 20:26:01 +01:00
|
|
|
iterateChildren(nodep);
|
|
|
|
|
}
|
2023-09-14 13:22:49 +02:00
|
|
|
void visit(AstNode* nodep) override { iterateChildren(nodep); }
|
2006-08-26 13:35:28 +02:00
|
|
|
|
|
|
|
|
public:
|
2019-09-12 13:22:22 +02:00
|
|
|
// CONSTRUCTORS
|
2026-06-12 15:15:41 +02:00
|
|
|
explicit CaseVisitor(AstNetlist* nodep) { iterate(nodep); }
|
2022-09-16 12:22:11 +02:00
|
|
|
~CaseVisitor() override {
|
2019-05-19 22:13:13 +02:00
|
|
|
V3Stats::addStat("Optimizations, Cases parallelized", m_statCaseFast);
|
|
|
|
|
V3Stats::addStat("Optimizations, Cases complex", m_statCaseSlow);
|
2006-08-26 13:35:28 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
//######################################################################
|
|
|
|
|
// Case class functions
|
|
|
|
|
|
|
|
|
|
void V3Case::caseAll(AstNetlist* nodep) {
|
2025-05-23 02:29:32 +02:00
|
|
|
UINFO(2, __FUNCTION__ << ":");
|
2021-11-26 16:52:36 +01:00
|
|
|
{ CaseVisitor{nodep}; } // Destruct before checking
|
2024-01-09 16:35:13 +01:00
|
|
|
V3Global::dumpCheckGlobalTree("case", 0, dumpTreeEitherLevel() >= 3);
|
2006-08-26 13:35:28 +02:00
|
|
|
}
|
2025-09-23 20:49:01 +02:00
|
|
|
void V3Case::caseLint(AstGenCase* nodep) {
|
2025-05-23 02:29:32 +02:00
|
|
|
UINFO(4, __FUNCTION__ << ": ");
|
2026-06-12 15:15:41 +02:00
|
|
|
CaseLintVisitor::apply(nodep);
|
2006-08-26 13:35:28 +02:00
|
|
|
}
|