mirror of https://github.com/llvm/circt.git
771 lines
27 KiB
C++
771 lines
27 KiB
C++
//===- ExpandWhens.cpp - Expand WhenOps into muxed operations ---*- C++ -*-===//
|
|
//
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This file defines the ExpandWhens pass.
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "PassDetails.h"
|
|
#include "circt/Dialect/FIRRTL/FIRRTLOps.h"
|
|
#include "circt/Dialect/FIRRTL/FIRRTLTypes.h"
|
|
#include "circt/Dialect/FIRRTL/FIRRTLUtils.h"
|
|
#include "circt/Dialect/FIRRTL/FIRRTLVisitors.h"
|
|
#include "circt/Dialect/FIRRTL/Passes.h"
|
|
#include "circt/Support/FieldRef.h"
|
|
#include "llvm/ADT/MapVector.h"
|
|
#include "llvm/ADT/STLExtras.h"
|
|
|
|
using namespace circt;
|
|
using namespace firrtl;
|
|
|
|
/// Move all operations from a source block in to a destination block. Leaves
|
|
/// the source block empty.
|
|
static void mergeBlock(Block &destination, Block::iterator insertPoint,
|
|
Block &source) {
|
|
destination.getOperations().splice(insertPoint, source.getOperations());
|
|
}
|
|
|
|
/// This is a stack of hashtables, if lookup fails in the top-most hashtable,
|
|
/// it will attempt to lookup in lower hashtables. This class is used instead
|
|
/// of a ScopedHashTable so we can manually pop off a scope and keep it around.
|
|
///
|
|
/// This only allows inserting into the outermost scope.
|
|
template <typename KeyT, typename ValueT>
|
|
struct HashTableStack {
|
|
using ScopeT = typename llvm::MapVector<KeyT, ValueT>;
|
|
using StackT = typename llvm::SmallVector<ScopeT, 3>;
|
|
|
|
struct Iterator {
|
|
Iterator(typename StackT::iterator stackIt,
|
|
typename ScopeT::iterator scopeIt)
|
|
: stackIt(stackIt), scopeIt(scopeIt) {}
|
|
|
|
bool operator==(const Iterator &rhs) const {
|
|
return stackIt == rhs.stackIt && scopeIt == rhs.scopeIt;
|
|
}
|
|
|
|
bool operator!=(const Iterator &rhs) const { return !(*this == rhs); }
|
|
|
|
std::pair<KeyT, ValueT> &operator*() const { return *scopeIt; }
|
|
|
|
Iterator &operator++() {
|
|
if (scopeIt == stackIt->end())
|
|
scopeIt = (++stackIt)->begin();
|
|
else
|
|
++scopeIt;
|
|
return *this;
|
|
}
|
|
|
|
typename StackT::iterator stackIt;
|
|
typename ScopeT::iterator scopeIt;
|
|
};
|
|
|
|
HashTableStack() {
|
|
// We require at least one scope.
|
|
pushScope();
|
|
}
|
|
|
|
using iterator = Iterator;
|
|
|
|
iterator begin() {
|
|
return Iterator(mapStack.begin(), mapStack.first().begin());
|
|
}
|
|
|
|
iterator end() { return Iterator(mapStack.end() - 1, mapStack.back().end()); }
|
|
|
|
iterator find(const KeyT &key) {
|
|
// Try to find a hashtable with the missing value.
|
|
for (auto i = mapStack.size(); i > 0; --i) {
|
|
auto &map = mapStack[i - 1];
|
|
auto it = map.find(key);
|
|
if (it != map.end())
|
|
return Iterator(mapStack.begin() + i - 1, it);
|
|
}
|
|
return end();
|
|
}
|
|
|
|
ScopeT &getLastScope() { return mapStack.back(); }
|
|
|
|
void pushScope() { mapStack.emplace_back(); }
|
|
|
|
ScopeT popScope() {
|
|
assert(mapStack.size() > 1 && "Cannot pop the last scope");
|
|
return mapStack.pop_back_val();
|
|
}
|
|
|
|
// This class lets you insert into the top scope.
|
|
ValueT &operator[](const KeyT &key) { return mapStack.back()[key]; }
|
|
|
|
private:
|
|
StackT mapStack;
|
|
};
|
|
|
|
/// This is a determistic mapping of a FieldRef to the last operation which set
|
|
/// a value to it.
|
|
using ScopedDriverMap = HashTableStack<FieldRef, Operation *>;
|
|
using DriverMap = ScopedDriverMap::ScopeT;
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// Last Connect Resolver
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
namespace {
|
|
/// This visitor visits process a block resolving last connect semantics
|
|
/// and recursively expanding WhenOps.
|
|
template <typename ConcreteT>
|
|
class LastConnectResolver : public FIRRTLVisitor<ConcreteT> {
|
|
protected:
|
|
/// Map of destinations and the operation which is driving a value to it in
|
|
/// the current scope. This is used for resolving last connect semantics, and
|
|
/// for retrieving the responsible connect operation.
|
|
ScopedDriverMap &driverMap;
|
|
|
|
public:
|
|
LastConnectResolver(ScopedDriverMap &driverMap) : driverMap(driverMap) {}
|
|
|
|
using FIRRTLVisitor<ConcreteT>::visitExpr;
|
|
using FIRRTLVisitor<ConcreteT>::visitDecl;
|
|
using FIRRTLVisitor<ConcreteT>::visitStmt;
|
|
|
|
/// Records a connection to a destination in the current scope.
|
|
/// If connection has static single connect behavior, this is all.
|
|
/// For connections with last-connect behavior, this will delete a previous
|
|
/// connection to a destination if there was one.
|
|
/// Returns true if an old connect was erased.
|
|
bool recordConnect(FieldRef dest, Operation *connection) {
|
|
// Try to insert, if it doesn't insert, replace the previous value.
|
|
auto itAndInserted = driverMap.getLastScope().insert({dest, connection});
|
|
if (isStaticSingleConnect(connection)) {
|
|
// There should be no non-null driver already, Verifier checks this.
|
|
assert(itAndInserted.second || !itAndInserted.first->second);
|
|
if (!itAndInserted.second)
|
|
itAndInserted.first->second = connection;
|
|
return false;
|
|
}
|
|
assert(isLastConnect(connection));
|
|
if (!std::get<1>(itAndInserted)) {
|
|
auto iterator = std::get<0>(itAndInserted);
|
|
auto changed = false;
|
|
// Delete the old connection if it exists. Null connections are inserted
|
|
// on declarations.
|
|
if (auto *oldConnect = iterator->second) {
|
|
oldConnect->erase();
|
|
changed = true;
|
|
}
|
|
iterator->second = connection;
|
|
return changed;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Get the destination value from a connection. This supports any operation
|
|
/// which is capable of driving a value.
|
|
static Value getDestinationValue(Operation *op) {
|
|
return cast<FConnectLike>(op).getDest();
|
|
}
|
|
|
|
/// Get the source value from a connection. This supports any operation which
|
|
/// is capable of driving a value.
|
|
static Value getConnectedValue(Operation *op) {
|
|
return cast<FConnectLike>(op).getSrc();
|
|
}
|
|
|
|
/// Return whether the connection has static single connection behavior.
|
|
static bool isStaticSingleConnect(Operation *op) {
|
|
return cast<FConnectLike>(op).hasStaticSingleConnectBehavior();
|
|
}
|
|
|
|
/// Return whether the connection has last-connect behavior.
|
|
/// Compared to static single connect behavior, with last-connect behavior
|
|
/// destinations can be connected to multiple times and are connected
|
|
/// conditionally when connecting out from under a 'when'.
|
|
static bool isLastConnect(Operation *op) {
|
|
return cast<FConnectLike>(op).hasLastConnectBehavior();
|
|
}
|
|
|
|
/// For every leaf field in the sink, record that it exists and should be
|
|
/// initialized.
|
|
void declareSinks(Value value, Flow flow, bool local = false) {
|
|
auto type = value.getType();
|
|
unsigned id = 0;
|
|
|
|
// Recurse through a bundle and declare each leaf sink node.
|
|
std::function<void(Type, Flow, bool)> declare = [&](Type type, Flow flow,
|
|
bool local) {
|
|
// If this is a class type, recurse to each of the fields.
|
|
if (auto classType = type_dyn_cast<ClassType>(type)) {
|
|
if (local) {
|
|
// If this is a local object declaration, then we are responsible for
|
|
// initializing its input ports.
|
|
for (auto &element : classType.getElements()) {
|
|
id++;
|
|
if (element.direction == Direction::Out)
|
|
declare(element.type, flow, false);
|
|
else
|
|
declare(element.type, swapFlow(flow), false);
|
|
}
|
|
} else {
|
|
// If this is a remote object, then the object itself is potentially
|
|
// a sink.
|
|
if (flow != Flow::Source)
|
|
driverMap[{value, id}] = nullptr;
|
|
// skip over its subfields--we are not responsible for initializing
|
|
// the object here.
|
|
id += classType.getMaxFieldID();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// If this is a bundle type, recurse to each of the fields.
|
|
if (auto bundleType = type_dyn_cast<BundleType>(type)) {
|
|
for (auto &element : bundleType.getElements()) {
|
|
id++;
|
|
if (element.isFlip)
|
|
declare(element.type, swapFlow(flow), false);
|
|
else
|
|
declare(element.type, flow, false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// If this is a vector type, recurse to each of the elements.
|
|
if (auto vectorType = type_dyn_cast<FVectorType>(type)) {
|
|
for (unsigned i = 0; i < vectorType.getNumElements(); ++i) {
|
|
id++;
|
|
declare(vectorType.getElementType(), flow, false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// If this is an analog type, it does not need to be tracked.
|
|
if (auto analogType = type_dyn_cast<AnalogType>(type))
|
|
return;
|
|
|
|
// If it is a leaf node with Flow::Sink or Flow::Duplex, it must be
|
|
// initialized.
|
|
if (flow != Flow::Source)
|
|
driverMap[{value, id}] = nullptr;
|
|
};
|
|
|
|
declare(type, flow, local);
|
|
}
|
|
|
|
/// Take two connection operations and merge them into a new connect under a
|
|
/// condition. Destination of both connects should be `dest`.
|
|
ConnectOp flattenConditionalConnections(OpBuilder &b, Location loc,
|
|
Value dest, Value cond,
|
|
Operation *whenTrueConn,
|
|
Operation *whenFalseConn) {
|
|
assert(isLastConnect(whenTrueConn) && isLastConnect(whenFalseConn));
|
|
auto fusedLoc =
|
|
b.getFusedLoc({loc, whenTrueConn->getLoc(), whenFalseConn->getLoc()});
|
|
auto whenTrue = getConnectedValue(whenTrueConn);
|
|
auto trueIsInvalid =
|
|
isa_and_nonnull<InvalidValueOp>(whenTrue.getDefiningOp());
|
|
auto whenFalse = getConnectedValue(whenFalseConn);
|
|
auto falseIsInvalid =
|
|
isa_and_nonnull<InvalidValueOp>(whenFalse.getDefiningOp());
|
|
// If one of the branches of the mux is an invalid value, we optimize the
|
|
// mux to be the non-invalid value. This optimization can only be
|
|
// performed while lowering when-ops into muxes, and would not be legal as
|
|
// a more general mux folder.
|
|
// mux(cond, invalid, x) -> x
|
|
// mux(cond, x, invalid) -> x
|
|
Value newValue = whenTrue;
|
|
if (trueIsInvalid == falseIsInvalid)
|
|
newValue = b.createOrFold<MuxPrimOp>(fusedLoc, cond, whenTrue, whenFalse);
|
|
else if (trueIsInvalid)
|
|
newValue = whenFalse;
|
|
return b.create<ConnectOp>(loc, dest, newValue);
|
|
}
|
|
|
|
void visitDecl(WireOp op) { declareSinks(op.getResult(), Flow::Duplex); }
|
|
|
|
/// Take an aggregate value and construct ground subelements recursively.
|
|
/// And then apply function `fn`.
|
|
void foreachSubelement(OpBuilder &builder, Value value,
|
|
llvm::function_ref<void(Value)> fn) {
|
|
FIRRTLTypeSwitch<Type>(value.getType())
|
|
.template Case<BundleType>([&](BundleType bundle) {
|
|
for (auto i : llvm::seq(0u, (unsigned)bundle.getNumElements())) {
|
|
auto subfield =
|
|
builder.create<SubfieldOp>(value.getLoc(), value, i);
|
|
foreachSubelement(builder, subfield, fn);
|
|
}
|
|
})
|
|
.template Case<FVectorType>([&](FVectorType vector) {
|
|
for (auto i : llvm::seq((size_t)0, vector.getNumElements())) {
|
|
auto subindex =
|
|
builder.create<SubindexOp>(value.getLoc(), value, i);
|
|
foreachSubelement(builder, subindex, fn);
|
|
}
|
|
})
|
|
.Default([&](auto) { fn(value); });
|
|
}
|
|
|
|
void visitDecl(RegOp op) {
|
|
// Registers are initialized to themselves. If the register has an
|
|
// aggergate type, connect each ground type element.
|
|
auto builder = OpBuilder(op->getBlock(), ++Block::iterator(op));
|
|
auto fn = [&](Value value) {
|
|
auto connect = builder.create<ConnectOp>(value.getLoc(), value, value);
|
|
driverMap[getFieldRefFromValue(value)] = connect;
|
|
};
|
|
foreachSubelement(builder, op.getResult(), fn);
|
|
}
|
|
|
|
void visitDecl(RegResetOp op) {
|
|
// Registers are initialized to themselves. If the register has an
|
|
// aggergate type, connect each ground type element.
|
|
auto builder = OpBuilder(op->getBlock(), ++Block::iterator(op));
|
|
auto fn = [&](Value value) {
|
|
auto connect = builder.create<ConnectOp>(value.getLoc(), value, value);
|
|
driverMap[getFieldRefFromValue(value)] = connect;
|
|
};
|
|
foreachSubelement(builder, op.getResult(), fn);
|
|
}
|
|
|
|
void visitDecl(InstanceOp op) {
|
|
// Track any instance inputs which need to be connected to for init
|
|
// coverage.
|
|
for (const auto &result : llvm::enumerate(op.getResults()))
|
|
if (op.getPortDirection(result.index()) == Direction::Out)
|
|
declareSinks(result.value(), Flow::Source);
|
|
else
|
|
declareSinks(result.value(), Flow::Sink);
|
|
}
|
|
|
|
void visitDecl(ObjectOp op) {
|
|
declareSinks(op, Flow::Source, /*local=*/true);
|
|
}
|
|
|
|
void visitDecl(MemOp op) {
|
|
// Track any memory inputs which require connections.
|
|
for (auto result : op.getResults())
|
|
if (!isa<RefType>(result.getType()))
|
|
declareSinks(result, Flow::Sink);
|
|
}
|
|
|
|
void visitStmt(ConnectOp op) {
|
|
recordConnect(getFieldRefFromValue(op.getDest()), op);
|
|
}
|
|
|
|
void visitStmt(StrictConnectOp op) {
|
|
recordConnect(getFieldRefFromValue(op.getDest()), op);
|
|
}
|
|
|
|
void visitStmt(RefDefineOp op) {
|
|
recordConnect(getFieldRefFromValue(op.getDest()), op);
|
|
}
|
|
|
|
void visitStmt(PropAssignOp op) {
|
|
recordConnect(getFieldRefFromValue(op.getDest()), op);
|
|
}
|
|
|
|
void processWhenOp(WhenOp whenOp, Value outerCondition);
|
|
|
|
/// Combine the connect statements from each side of the block. There are 5
|
|
/// cases to consider. If all are set, last connect semantics dictate that it
|
|
/// is actually the third case.
|
|
///
|
|
/// Prev | Then | Else | Outcome
|
|
/// -----|------|------|-------
|
|
/// | set | | then
|
|
/// | | set | else
|
|
/// set | set | set | mux(p, then, else)
|
|
/// | set | set | impossible
|
|
/// set | set | | mux(p, then, prev)
|
|
/// set | | set | mux(p, prev, else)
|
|
///
|
|
/// If the value was declared in the block, then it does not need to have been
|
|
/// assigned a previous value. If the value was declared before the block,
|
|
/// then there is an incomplete initialization error.
|
|
void mergeScopes(Location loc, DriverMap &thenScope, DriverMap &elseScope,
|
|
Value thenCondition) {
|
|
|
|
// Process all connects in the `then` block.
|
|
for (auto &destAndConnect : thenScope) {
|
|
auto dest = std::get<0>(destAndConnect);
|
|
auto thenConnect = std::get<1>(destAndConnect);
|
|
|
|
auto outerIt = driverMap.find(dest);
|
|
if (outerIt == driverMap.end()) {
|
|
// `dest` is set in `then` only. This indicates it was created in the
|
|
// `then` block, so just copy it into the outer scope.
|
|
driverMap[dest] = thenConnect;
|
|
continue;
|
|
}
|
|
|
|
auto elseIt = elseScope.find(dest);
|
|
if (elseIt != elseScope.end()) {
|
|
// `dest` is set in `then` and `else`. We need to combine them into and
|
|
// delete any previous connect.
|
|
|
|
// Create a new connect with `mux(p, then, else)`.
|
|
auto &elseConnect = std::get<1>(*elseIt);
|
|
OpBuilder connectBuilder(elseConnect);
|
|
auto newConnect = flattenConditionalConnections(
|
|
connectBuilder, loc, getDestinationValue(thenConnect),
|
|
thenCondition, thenConnect, elseConnect);
|
|
|
|
// Delete all old connections.
|
|
thenConnect->erase();
|
|
elseConnect->erase();
|
|
recordConnect(dest, newConnect);
|
|
|
|
continue;
|
|
}
|
|
|
|
auto &outerConnect = std::get<1>(*outerIt);
|
|
if (!outerConnect) {
|
|
if (isLastConnect(thenConnect)) {
|
|
// `dest` is null in the outer scope. This indicate an initialization
|
|
// problem: `mux(p, then, nullptr)`. Just delete the broken connect.
|
|
thenConnect->erase();
|
|
} else {
|
|
assert(isStaticSingleConnect(thenConnect));
|
|
driverMap[dest] = thenConnect;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// `dest` is set in `then` and the outer scope. Create a new connect with
|
|
// `mux(p, then, outer)`.
|
|
OpBuilder connectBuilder(thenConnect);
|
|
auto newConnect = flattenConditionalConnections(
|
|
connectBuilder, loc, getDestinationValue(thenConnect), thenCondition,
|
|
thenConnect, outerConnect);
|
|
|
|
// Delete all old connections.
|
|
thenConnect->erase();
|
|
recordConnect(dest, newConnect);
|
|
}
|
|
|
|
// Process all connects in the `else` block.
|
|
for (auto &destAndConnect : elseScope) {
|
|
auto dest = std::get<0>(destAndConnect);
|
|
auto elseConnect = std::get<1>(destAndConnect);
|
|
|
|
// If this destination was driven in the 'then' scope, then we will have
|
|
// already consumed the driver from the 'else' scope, and we must skip it.
|
|
if (thenScope.contains(dest))
|
|
continue;
|
|
|
|
auto outerIt = driverMap.find(dest);
|
|
if (outerIt == driverMap.end()) {
|
|
// `dest` is set in `else` only. This indicates it was created in the
|
|
// `else` block, so just copy it into the outer scope.
|
|
driverMap[dest] = elseConnect;
|
|
continue;
|
|
}
|
|
|
|
auto &outerConnect = std::get<1>(*outerIt);
|
|
if (!outerConnect) {
|
|
if (isLastConnect(elseConnect)) {
|
|
// `dest` is null in the outer scope. This indicates an initialization
|
|
// problem: `mux(p, then, nullptr)`. Just delete the broken connect.
|
|
elseConnect->erase();
|
|
} else {
|
|
assert(isStaticSingleConnect(elseConnect));
|
|
driverMap[dest] = elseConnect;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// `dest` is set in the `else` and outer scope. Create a new connect with
|
|
// `mux(p, outer, else)`.
|
|
OpBuilder connectBuilder(elseConnect);
|
|
auto newConnect = flattenConditionalConnections(
|
|
connectBuilder, loc, getDestinationValue(outerConnect), thenCondition,
|
|
outerConnect, elseConnect);
|
|
|
|
// Delete all old connections.
|
|
elseConnect->erase();
|
|
recordConnect(dest, newConnect);
|
|
}
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// WhenOpVisitor
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
/// This extends the LastConnectVisitor to handle all Simulation related
|
|
/// constructs which do not need any processing at the module scope, but need to
|
|
/// be processed inside of a WhenOp.
|
|
namespace {
|
|
class WhenOpVisitor : public LastConnectResolver<WhenOpVisitor> {
|
|
|
|
public:
|
|
WhenOpVisitor(ScopedDriverMap &driverMap, Value condition)
|
|
: LastConnectResolver<WhenOpVisitor>(driverMap), condition(condition) {}
|
|
|
|
using LastConnectResolver<WhenOpVisitor>::visitExpr;
|
|
using LastConnectResolver<WhenOpVisitor>::visitDecl;
|
|
using LastConnectResolver<WhenOpVisitor>::visitStmt;
|
|
|
|
/// Process a block, recording each declaration, and expanding all whens.
|
|
void process(Block &block);
|
|
|
|
/// Simulation Constructs.
|
|
void visitStmt(AssertOp op);
|
|
void visitStmt(AssumeOp op);
|
|
void visitStmt(CoverOp op);
|
|
void visitStmt(ModuleOp op);
|
|
void visitStmt(PrintFOp op);
|
|
void visitStmt(StopOp op);
|
|
void visitStmt(WhenOp op);
|
|
void visitStmt(RefForceOp op);
|
|
void visitStmt(RefForceInitialOp op);
|
|
void visitStmt(RefReleaseOp op);
|
|
void visitStmt(RefReleaseInitialOp op);
|
|
|
|
private:
|
|
/// And a 1-bit value with the current condition. If we are in the outer
|
|
/// scope, i.e. not in a WhenOp region, then there is no condition.
|
|
Value andWithCondition(Operation *op, Value value) {
|
|
// 'and' the value with the current condition.
|
|
return OpBuilder(op).createOrFold<AndPrimOp>(
|
|
condition.getLoc(), condition.getType(), condition, value);
|
|
}
|
|
|
|
private:
|
|
/// The current wrapping condition. If null, we are in the outer scope.
|
|
Value condition;
|
|
};
|
|
} // namespace
|
|
|
|
void WhenOpVisitor::process(Block &block) {
|
|
for (auto &op : llvm::make_early_inc_range(block)) {
|
|
dispatchVisitor(&op);
|
|
}
|
|
}
|
|
|
|
void WhenOpVisitor::visitStmt(PrintFOp op) {
|
|
op.getCondMutable().assign(andWithCondition(op, op.getCond()));
|
|
}
|
|
|
|
void WhenOpVisitor::visitStmt(StopOp op) {
|
|
op.getCondMutable().assign(andWithCondition(op, op.getCond()));
|
|
}
|
|
|
|
void WhenOpVisitor::visitStmt(AssertOp op) {
|
|
op.getEnableMutable().assign(andWithCondition(op, op.getEnable()));
|
|
}
|
|
|
|
void WhenOpVisitor::visitStmt(AssumeOp op) {
|
|
op.getEnableMutable().assign(andWithCondition(op, op.getEnable()));
|
|
}
|
|
|
|
void WhenOpVisitor::visitStmt(CoverOp op) {
|
|
op.getEnableMutable().assign(andWithCondition(op, op.getEnable()));
|
|
}
|
|
|
|
void WhenOpVisitor::visitStmt(WhenOp whenOp) {
|
|
processWhenOp(whenOp, condition);
|
|
}
|
|
|
|
void WhenOpVisitor::visitStmt(RefForceOp op) {
|
|
op.getPredicateMutable().assign(andWithCondition(op, op.getPredicate()));
|
|
}
|
|
|
|
void WhenOpVisitor::visitStmt(RefForceInitialOp op) {
|
|
op.getPredicateMutable().assign(andWithCondition(op, op.getPredicate()));
|
|
}
|
|
|
|
void WhenOpVisitor::visitStmt(RefReleaseOp op) {
|
|
op.getPredicateMutable().assign(andWithCondition(op, op.getPredicate()));
|
|
}
|
|
|
|
void WhenOpVisitor::visitStmt(RefReleaseInitialOp op) {
|
|
op.getPredicateMutable().assign(andWithCondition(op, op.getPredicate()));
|
|
}
|
|
|
|
/// This is a common helper that is dispatched to by the concrete visitors.
|
|
/// This condition should be the conjunction of all surrounding WhenOp
|
|
/// condititions.
|
|
///
|
|
/// This requires WhenOpVisitor to be fully defined.
|
|
template <typename ConcreteT>
|
|
void LastConnectResolver<ConcreteT>::processWhenOp(WhenOp whenOp,
|
|
Value outerCondition) {
|
|
OpBuilder b(whenOp);
|
|
auto loc = whenOp.getLoc();
|
|
Block *parentBlock = whenOp->getBlock();
|
|
auto condition = whenOp.getCondition();
|
|
auto ui1Type = condition.getType();
|
|
|
|
// Process both sides of the WhenOp, fixing up all simulation constructs,
|
|
// and resolving last connect semantics in each block. This process returns
|
|
// the set of connects in each side of the when op.
|
|
|
|
// Process the `then` block. If we are already in a whenblock, the we need to
|
|
// conjoin ('and') the outer conditions.
|
|
Value thenCondition = whenOp.getCondition();
|
|
if (outerCondition)
|
|
thenCondition =
|
|
b.createOrFold<AndPrimOp>(loc, ui1Type, outerCondition, thenCondition);
|
|
|
|
auto &thenBlock = whenOp.getThenBlock();
|
|
driverMap.pushScope();
|
|
WhenOpVisitor(driverMap, thenCondition).process(thenBlock);
|
|
mergeBlock(*parentBlock, Block::iterator(whenOp), thenBlock);
|
|
auto thenScope = driverMap.popScope();
|
|
|
|
// Process the `else` block.
|
|
DriverMap elseScope;
|
|
if (whenOp.hasElseRegion()) {
|
|
// Else condition is the complement of the then condition.
|
|
auto elseCondition =
|
|
b.createOrFold<NotPrimOp>(loc, condition.getType(), condition);
|
|
// Conjoin the when condition with the outer condition.
|
|
if (outerCondition)
|
|
elseCondition = b.createOrFold<AndPrimOp>(loc, ui1Type, outerCondition,
|
|
elseCondition);
|
|
auto &elseBlock = whenOp.getElseBlock();
|
|
driverMap.pushScope();
|
|
WhenOpVisitor(driverMap, elseCondition).process(elseBlock);
|
|
mergeBlock(*parentBlock, Block::iterator(whenOp), elseBlock);
|
|
elseScope = driverMap.popScope();
|
|
}
|
|
|
|
mergeScopes(loc, thenScope, elseScope, condition);
|
|
|
|
// Delete the now empty WhenOp.
|
|
whenOp.erase();
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// ModuleOpVisitor
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
namespace {
|
|
/// This extends the LastConnectResolver to track if anything has changed.
|
|
class ModuleVisitor : public LastConnectResolver<ModuleVisitor> {
|
|
public:
|
|
ModuleVisitor() : LastConnectResolver<ModuleVisitor>(driverMap) {}
|
|
|
|
using LastConnectResolver<ModuleVisitor>::visitExpr;
|
|
using LastConnectResolver<ModuleVisitor>::visitDecl;
|
|
using LastConnectResolver<ModuleVisitor>::visitStmt;
|
|
void visitStmt(WhenOp whenOp);
|
|
void visitStmt(ConnectOp connectOp);
|
|
void visitStmt(StrictConnectOp connectOp);
|
|
void visitStmt(LayerBlockOp layerBlockOp);
|
|
|
|
bool run(FModuleLike op);
|
|
LogicalResult checkInitialization();
|
|
|
|
private:
|
|
/// The outermost scope of the module body.
|
|
ScopedDriverMap driverMap;
|
|
|
|
/// Tracks if anything in the IR has changed.
|
|
bool anythingChanged = false;
|
|
};
|
|
} // namespace
|
|
|
|
/// Run expand whens on the Module. This will emit an error for each
|
|
/// incomplete initialization found. If an initialiazation error was detected,
|
|
/// this will return failure and leave the IR in an inconsistent state.
|
|
bool ModuleVisitor::run(FModuleLike op) {
|
|
// We only lower whens inside of fmodule ops or class ops.
|
|
if (!isa<FModuleOp, ClassOp>(op))
|
|
return anythingChanged;
|
|
|
|
for (auto ®ion : op->getRegions()) {
|
|
for (auto &block : region.getBlocks()) {
|
|
// Track any results (flipped arguments) of the module for init coverage.
|
|
for (const auto &[index, value] : llvm::enumerate(block.getArguments())) {
|
|
auto direction = op.getPortDirection(index);
|
|
auto flow = direction == Direction::In ? Flow::Source : Flow::Sink;
|
|
declareSinks(value, flow);
|
|
}
|
|
|
|
// Process the body of the module.
|
|
for (auto &op : llvm::make_early_inc_range(block))
|
|
dispatchVisitor(&op);
|
|
}
|
|
}
|
|
|
|
return anythingChanged;
|
|
}
|
|
|
|
void ModuleVisitor::visitStmt(ConnectOp op) {
|
|
anythingChanged |= recordConnect(getFieldRefFromValue(op.getDest()), op);
|
|
}
|
|
|
|
void ModuleVisitor::visitStmt(StrictConnectOp op) {
|
|
anythingChanged |= recordConnect(getFieldRefFromValue(op.getDest()), op);
|
|
}
|
|
|
|
void ModuleVisitor::visitStmt(WhenOp whenOp) {
|
|
// If we are deleting a WhenOp something definitely changed.
|
|
anythingChanged = true;
|
|
processWhenOp(whenOp, /*outerCondition=*/{});
|
|
}
|
|
|
|
void ModuleVisitor::visitStmt(LayerBlockOp layerBlockOp) {
|
|
for (auto &op : llvm::make_early_inc_range(*layerBlockOp.getBody())) {
|
|
dispatchVisitor(&op);
|
|
}
|
|
}
|
|
|
|
/// Perform initialization checking. This uses the built up state from
|
|
/// running on a module. Returns failure in the event of bad initialization.
|
|
LogicalResult ModuleVisitor::checkInitialization() {
|
|
bool failed = false;
|
|
for (auto destAndConnect : driverMap.getLastScope()) {
|
|
// If there is valid connection to this destination, everything is good.
|
|
auto *connect = std::get<1>(destAndConnect);
|
|
if (connect)
|
|
continue;
|
|
|
|
// Get the op which defines the sink, and emit an error.
|
|
FieldRef dest = std::get<0>(destAndConnect);
|
|
auto loc = dest.getValue().getLoc();
|
|
auto *definingOp = dest.getDefiningOp();
|
|
if (auto mod = dyn_cast<FModuleLike>(definingOp))
|
|
mlir::emitError(loc) << "port \"" << getFieldName(dest).first
|
|
<< "\" not fully initialized in \""
|
|
<< mod.getModuleName() << "\"";
|
|
else
|
|
mlir::emitError(loc)
|
|
<< "sink \"" << getFieldName(dest).first
|
|
<< "\" not fully initialized in \""
|
|
<< definingOp->getParentOfType<FModuleLike>().getModuleName() << "\"";
|
|
failed = true;
|
|
}
|
|
if (failed)
|
|
return failure();
|
|
return success();
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// Pass Infrastructure
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
namespace {
|
|
class ExpandWhensPass : public ExpandWhensBase<ExpandWhensPass> {
|
|
void runOnOperation() override;
|
|
};
|
|
} // end anonymous namespace
|
|
|
|
void ExpandWhensPass::runOnOperation() {
|
|
ModuleVisitor visitor;
|
|
if (!visitor.run(getOperation()))
|
|
markAllAnalysesPreserved();
|
|
if (failed(visitor.checkInitialization()))
|
|
signalPassFailure();
|
|
}
|
|
|
|
std::unique_ptr<mlir::Pass> circt::firrtl::createExpandWhensPass() {
|
|
return std::make_unique<ExpandWhensPass>();
|
|
}
|