circt/lib/Dialect/Calyx/Export/CalyxEmitter.cpp

1208 lines
44 KiB
C++

//===- CalyxEmitter.cpp - Calyx dialect to .futil emitter -----------------===//
//
// 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 implements an emitter for the native Calyx language, which uses
// .futil as an alias.
//
//===----------------------------------------------------------------------===//
#include "circt/Dialect/Calyx/CalyxEmitter.h"
#include "circt/Dialect/Calyx/CalyxOps.h"
#include "circt/Dialect/Comb/CombOps.h"
#include "circt/Dialect/HW/HWAttributes.h"
#include "circt/Dialect/HW/HWOps.h"
#include "circt/Support/LLVM.h"
#include "mlir/IR/BuiltinOps.h"
#include "mlir/IR/BuiltinTypes.h"
#include "mlir/Tools/mlir-translate/Translation.h"
#include "llvm/ADT/SmallSet.h"
#include "llvm/ADT/TypeSwitch.h"
#include "llvm/Support/Casting.h"
#include "llvm/Support/FormatVariadic.h"
#include <bitset>
#include <string>
using namespace circt;
using namespace calyx;
using namespace mlir;
namespace {
static constexpr std::string_view LSquare() { return "["; }
static constexpr std::string_view RSquare() { return "]"; }
static constexpr std::string_view LAngleBracket() { return "<"; }
static constexpr std::string_view RAngleBracket() { return ">"; }
static constexpr std::string_view LParen() { return "("; }
static constexpr std::string_view RParen() { return ")"; }
static constexpr std::string_view colon() { return ": "; }
static constexpr std::string_view space() { return " "; }
static constexpr std::string_view period() { return "."; }
static constexpr std::string_view questionMark() { return " ? "; }
static constexpr std::string_view exclamationMark() { return "!"; }
static constexpr std::string_view equals() { return "="; }
static constexpr std::string_view comma() { return ", "; }
static constexpr std::string_view arrow() { return " -> "; }
static constexpr std::string_view quote() { return "\""; }
static constexpr std::string_view apostrophe() { return "'"; }
static constexpr std::string_view LBraceEndL() { return "{\n"; }
static constexpr std::string_view RBraceEndL() { return "}\n"; }
static constexpr std::string_view semicolonEndL() { return ";\n"; }
static constexpr std::string_view addressSymbol() { return "@"; }
static constexpr std::string_view endl() { return "\n"; }
static constexpr std::string_view metadataLBrace() { return "#{\n"; }
static constexpr std::string_view metadataRBrace() { return "}#\n"; }
/// A list of integer attributes supported by the native Calyx compiler.
constexpr std::array<StringRef, 7> integerAttributes{
"external", "static", "share", "bound",
"write_together", "read_together", "pos",
};
/// A list of boolean attributes supported by the native Calyx compiler.
constexpr std::array<StringRef, 12> booleanAttributes{
"clk", "reset", "go", "done", "generated", "precious",
"toplevel", "stable", "nointerface", "inline", "state_share", "data",
};
static std::optional<StringRef> getCalyxAttrIdentifier(NamedAttribute attr) {
StringRef identifier = attr.getName().strref();
if (identifier.contains(".")) {
Dialect *dialect = attr.getNameDialect();
if (dialect != nullptr && isa<CalyxDialect>(*dialect)) {
return std::get<1>(identifier.split("."));
}
return std::nullopt;
}
return identifier;
}
/// Determines whether the given identifier is a valid Calyx attribute.
static bool isValidCalyxAttribute(StringRef identifier) {
return llvm::find(integerAttributes, identifier) != integerAttributes.end() ||
llvm::find(booleanAttributes, identifier) != booleanAttributes.end();
}
/// A tracker to determine which libraries should be imported for a given
/// program.
struct ImportTracker {
public:
/// Returns the list of library names used for in this program.
/// E.g. if `primitives/core.futil` is used, returns { "core" }.
FailureOr<llvm::SmallSet<StringRef, 4>> getLibraryNames(ModuleOp module) {
auto walkRes = module.walk([&](ComponentOp component) {
for (auto &op : *component.getBodyBlock()) {
if (!isa<CellInterface>(op) || isa<InstanceOp, PrimitiveOp>(op))
// It is not a primitive.
continue;
auto libraryName = getLibraryFor(&op);
if (failed(libraryName))
return WalkResult::interrupt();
usedLibraries.insert(*libraryName);
}
return WalkResult::advance();
});
if (walkRes.wasInterrupted())
return failure();
return usedLibraries;
}
private:
/// Returns the library name for a given Operation Type.
FailureOr<StringRef> getLibraryFor(Operation *op) {
return TypeSwitch<Operation *, FailureOr<StringRef>>(op)
.Case<AddLibOp, RegisterOp, UndefLibOp, WireLibOp>(
[&](auto op) -> FailureOr<StringRef> {
static constexpr std::string_view sCompile = "compile";
return {sCompile};
})
.Case<NotLibOp, AndLibOp, OrLibOp, XorLibOp, SubLibOp, GtLibOp, LtLibOp,
EqLibOp, NeqLibOp, GeLibOp, LeLibOp, LshLibOp, RshLibOp,
SliceLibOp, PadLibOp, MuxLibOp>(
[&](auto op) -> FailureOr<StringRef> {
static constexpr std::string_view sCore = "core";
return {sCore};
})
.Case<SgtLibOp, SltLibOp, SeqLibOp, SneqLibOp, SgeLibOp, SleLibOp,
SrshLibOp, MultPipeLibOp, RemUPipeLibOp, RemSPipeLibOp,
DivUPipeLibOp, DivSPipeLibOp, ExtSILibOp>(
[&](auto op) -> FailureOr<StringRef> {
static constexpr std::string_view sBinaryOperators =
"binary_operators";
return {sBinaryOperators};
})
.Case<MemoryOp>([&](auto op) -> FailureOr<StringRef> {
static constexpr std::string_view sMemories = "memories/comb";
return {sMemories};
})
.Case<SeqMemoryOp>([&](auto op) -> FailureOr<StringRef> {
static constexpr std::string_view sMemories = "memories/seq";
return {sMemories};
})
.Case<ConstantOp>([&](auto op) -> FailureOr<StringRef> {
static constexpr std::string_view sFloat = "float";
return {sFloat};
})
.Case<AddFOpIEEE754>([&](auto op) -> FailureOr<StringRef> {
static constexpr std::string_view sFloatingPoint = "float/addFN";
return {sFloatingPoint};
})
.Case<MulFOpIEEE754>([&](auto op) -> FailureOr<StringRef> {
static constexpr std::string_view sFloatingPoint = "float/mulFN";
return {sFloatingPoint};
})
.Case<CompareFOpIEEE754>([&](auto op) -> FailureOr<StringRef> {
static constexpr std::string_view sFloatingPoint = "float/compareFN";
return {sFloatingPoint};
})
.Case<FpToIntOpIEEE754>([&](auto op) -> FailureOr<StringRef> {
static constexpr std::string_view sFloatingPoint = "float/fpToInt";
return {sFloatingPoint};
})
.Case<IntToFpOpIEEE754>([&](auto op) -> FailureOr<StringRef> {
static constexpr std::string_view sFloatingPoint = "float/intToFp";
return {sFloatingPoint};
})
.Case<DivSqrtOpIEEE754>([&](auto op) -> FailureOr<StringRef> {
static constexpr std::string_view sFloatingPoint = "float/divSqrtFN";
return {sFloatingPoint};
})
.Default([&](auto op) {
auto diag = op->emitOpError() << "not supported for emission";
return diag;
});
}
/// Maintains a unique list of libraries used throughout the lifetime of the
/// tracker.
llvm::SmallSet<StringRef, 4> usedLibraries;
};
//===----------------------------------------------------------------------===//
// Emitter
//===----------------------------------------------------------------------===//
/// An emitter for Calyx dialect operations to .futil output.
struct Emitter {
Emitter(llvm::raw_ostream &os) : os(os) {}
LogicalResult finalize();
// Indentation
raw_ostream &indent() { return os.indent(currentIndent); }
void addIndent() { currentIndent += 2; }
void reduceIndent() {
assert(currentIndent >= 2 && "Unintended indentation wrap");
currentIndent -= 2;
}
// Module emission
void emitModule(ModuleOp op);
// Metadata emission for the Cider debugger.
void emitCiderMetadata(mlir::ModuleOp op) {
auto metadata = op->getAttrOfType<ArrayAttr>("calyx.metadata");
if (!metadata)
return;
constexpr std::string_view metadataIdentifier = "metadata";
os << endl() << metadataIdentifier << space() << metadataLBrace();
for (auto sourceLoc : llvm::enumerate(metadata)) {
// <index>: <source-location>\n
os << std::to_string(sourceLoc.index()) << colon();
os << cast<StringAttr>(sourceLoc.value()).getValue() << endl();
}
os << metadataRBrace();
}
/// Import emission.
LogicalResult emitImports(ModuleOp op) {
auto emitImport = [&](StringRef library) {
// Libraries share a common relative path:
// primitives/<library-name>.futil
os << "import " << quote() << "primitives/" << library << period()
<< "futil" << quote() << semicolonEndL();
};
auto libraryNames = importTracker.getLibraryNames(op);
if (failed(libraryNames))
return failure();
for (StringRef library : *libraryNames)
emitImport(library);
return success();
}
// Component emission
void emitComponent(ComponentInterface op);
void emitComponentPorts(ComponentInterface op);
// HWModuleExtern emission
void emitPrimitiveExtern(hw::HWModuleExternOp op);
void emitPrimitivePorts(hw::HWModuleExternOp op);
// Instance emission
void emitInstance(InstanceOp op);
// Primitive emission
void emitPrimitive(PrimitiveOp op);
// Wires emission
void emitWires(WiresOp op);
// Group emission
void emitGroup(GroupInterface group);
// Control emission
void emitControl(ControlOp control);
// Assignment emission
void emitAssignment(AssignOp op);
// Enable emission
void emitEnable(EnableOp enable);
// Register emission
void emitRegister(RegisterOp reg);
// Emit undefined op
void emitUndef(UndefLibOp op);
// Memory emission
void emitMemory(MemoryOp memory);
// Seq Memory emission
void emitSeqMemory(SeqMemoryOp memory);
// Invoke emission
void emitInvoke(InvokeOp invoke);
// Floating point Constant emission
void emitConstant(ConstantOp constant);
// Emits a library primitive with template parameters based on all in- and
// output ports.
// e.g.:
// $f.in0, $f.in1, $f.in2, $f.out : calyx.std_foo "f" : i1, i2, i3, i4
// emits:
// f = std_foo(1, 2, 3, 4);
void emitLibraryPrimTypedByAllPorts(Operation *op);
// Emits a library primitive with a single template parameter based on the
// first input port.
// e.g.:
// $f.in0, $f.in1, $f.out : calyx.std_foo "f" : i32, i32, i1
// emits:
// f = std_foo(32);
void emitLibraryPrimTypedByFirstInputPort(Operation *op);
// Emits a library primitive with a single template parameter based on the
// first output port.
// e.g.:
// $f.in0, $f.in1, $f.out : calyx.std_foo "f" : i32, i32, i1
// emits:
// f = std_foo(1);
void emitLibraryPrimTypedByFirstOutputPort(
Operation *op, std::optional<StringRef> calyxLibName = {});
// Emits a library floating point primitives
void emitLibraryFloatingPoint(Operation *op);
private:
/// Used to track which imports are required for this program.
ImportTracker importTracker;
/// Emit an error and remark that emission failed.
InFlightDiagnostic emitError(Operation *op, const Twine &message) {
encounteredError = true;
return op->emitError(message);
}
/// Emit an error and remark that emission failed.
InFlightDiagnostic emitOpError(Operation *op, const Twine &message) {
encounteredError = true;
return op->emitOpError(message);
}
/// Calyx attributes are emitted in one of the two following formats:
/// (1) @<attribute-name>(<attribute-value>), e.g. `@go`, `@bound(5)`.
/// (2) <"<attribute-name>"=<attribute-value>>, e.g. `<"static"=1>`.
///
/// Since ports are structural in nature and not operations, an
/// extra boolean value is added to determine whether this is a port of the
/// given operation.
///
/// By default, this generates format (1) but can generate format (2) if
/// `at_format` is false.
std::string getAttribute(Operation *op, NamedAttribute attr, bool isPort,
bool atFormat) {
std::optional<StringRef> identifierOpt = getCalyxAttrIdentifier(attr);
// Verify this is a Calyx attribute
if (!identifierOpt.has_value())
return "";
StringRef identifier = *identifierOpt;
// Verify this attribute is supported for emission.
if (!isValidCalyxAttribute(identifier))
return "";
std::string output;
llvm::raw_string_ostream buffer(output);
buffer.reserveExtraSpace(32);
bool isBooleanAttribute =
llvm::find(booleanAttributes, identifier) != booleanAttributes.end();
if (isa<UnitAttr>(attr.getValue())) {
assert(isBooleanAttribute &&
"Non-boolean attributes must provide an integer value.");
if (!atFormat) {
buffer << quote() << identifier << quote() << equals() << "1";
} else {
buffer << addressSymbol() << identifier;
}
} else if (auto intAttr = dyn_cast<IntegerAttr>(attr.getValue())) {
APInt value = intAttr.getValue();
if (!atFormat) {
buffer << quote() << identifier << quote() << equals() << value;
} else {
buffer << addressSymbol() << identifier;
// The only time we may omit the value is when it is a Boolean attribute
// with value 1.
if (!isBooleanAttribute || intAttr.getValue() != 1) {
// Retrieve the unsigned representation of the value.
SmallVector<char, 4> s;
value.toStringUnsigned(s, /*Radix=*/10);
buffer << LParen() << s << RParen();
}
}
}
return buffer.str();
}
/// Emits the attributes of a dictionary. If the `attributes` dictionary is
/// not nullptr, we assume this is for a port.
std::string getAttributes(Operation *op, bool atFormat,
DictionaryAttr attributes = nullptr) {
bool isPort = attributes != nullptr;
bool atLeastOne = false;
if (!isPort)
attributes = op->getAttrDictionary();
std::string calyxAttributes;
llvm::raw_string_ostream buf(calyxAttributes);
if (!atFormat)
buf << LAngleBracket();
for (auto &attr : attributes) {
// If the output
if (auto out = getAttribute(op, attr, isPort, atFormat); !out.empty()) {
buf << out;
atLeastOne = true;
buf << (atFormat ? space() : comma());
}
}
if (atLeastOne) {
auto out = buf.str();
// Remove the last character which is an extra space or comma.
out.pop_back();
out.append(atFormat ? space() : RAngleBracket());
return out;
}
return "";
}
/// Helper function for emitting a Calyx section. It emits the body in the
/// following format:
/// {
/// <body>
/// }
template <typename Func>
void emitCalyxBody(Func emitBody) {
os << space() << LBraceEndL();
addIndent();
emitBody();
reduceIndent();
indent() << RBraceEndL();
}
/// Emits a Calyx section.
template <typename Func>
void emitCalyxSection(StringRef sectionName, Func emitBody,
StringRef symbolName = "") {
indent() << sectionName;
if (!symbolName.empty())
os << space() << symbolName;
emitCalyxBody(emitBody);
}
/// Helper function for emitting combinational operations.
template <typename CombinationalOp>
void emitCombinationalValue(CombinationalOp op, StringRef logicalSymbol) {
auto inputs = op.getInputs();
os << LParen();
for (size_t i = 0, e = inputs.size(); i != e; ++i) {
emitValue(inputs[i], /*isIndented=*/false);
if (i + 1 == e)
continue;
os << space() << logicalSymbol << space();
}
os << RParen();
}
void emitCycleValue(CycleOp op) {
os << "%";
if (op.getEnd().has_value()) {
os << LSquare();
os << op.getStart() << ":" << op.getEnd();
os << RSquare();
} else {
os << op.getStart();
}
}
/// Emits the value of a guard or assignment.
void emitValue(Value value, bool isIndented) {
if (auto blockArg = dyn_cast<BlockArgument>(value)) {
// Emit component block argument.
StringAttr portName = getPortInfo(blockArg).name;
(isIndented ? indent() : os) << portName.getValue();
return;
}
auto *definingOp = value.getDefiningOp();
assert(definingOp && "Value does not have a defining operation.");
TypeSwitch<Operation *>(definingOp)
.Case<CellInterface>([&](auto cell) {
// A cell port should be defined as <instance-name>.<port-name>
(isIndented ? indent() : os)
<< cell.instanceName() << period() << cell.portName(value);
})
.Case<hw::ConstantOp>([&](auto op) {
// A constant is defined as <bit-width>'<base><value>, where the base
// is `b` (binary), `o` (octal), `h` hexadecimal, or `d` (decimal).
APInt value = op.getValue();
auto &stream = isIndented ? indent() : os;
bool isNegative = value.isNegative();
StringRef base = isNegative ? "b" : "d";
stream << std::to_string(value.getBitWidth()) << apostrophe() << base;
if (isNegative) {
SmallString<8> str;
value.toStringUnsigned(str, /*Radix=*/2);
stream << str;
} else {
value.print(stream, /*isSigned=*/false);
}
})
.Case<comb::AndOp>([&](auto op) { emitCombinationalValue(op, "&"); })
.Case<comb::OrOp>([&](auto op) { emitCombinationalValue(op, "|"); })
.Case<comb::XorOp>([&](auto op) {
// The XorOp is a bit different, since the Combinational dialect
// uses it to represent binary not.
if (!op.isBinaryNot()) {
emitOpError(op, "Only supporting Binary Not for XOR.");
return;
}
// The LHS is the value to be negated, and the RHS is a constant with
// all ones (guaranteed by isBinaryNot).
os << exclamationMark();
emitValue(op.getInputs()[0], /*isIndented=*/false);
})
.Case<CycleOp>([&](auto op) { emitCycleValue(op); })
.Default(
[&](auto op) { emitOpError(op, "not supported for emission"); });
}
/// Emits a port for a Group.
template <typename OpTy>
void emitGroupPort(GroupInterface group, OpTy op, StringRef portHole) {
assert((isa<GroupGoOp>(op) || isa<GroupDoneOp>(op)) &&
"Required to be a group port.");
indent() << group.symName().getValue() << LSquare() << portHole << RSquare()
<< space() << equals() << space();
if (op.getGuard()) {
emitValue(op.getGuard(), /*isIndented=*/false);
os << questionMark();
}
emitValue(op.getSrc(), /*isIndented=*/false);
os << semicolonEndL();
}
/// Recursively emits the Calyx control.
void emitCalyxControl(Block *body) {
Operation *parent = body->getParentOp();
assert((isa<ControlOp>(parent) || parent->hasTrait<ControlLike>()) &&
"This should only be used to emit Calyx Control structures.");
// Check to see if this is a stand-alone EnableOp, i.e.
// calyx.control { calyx.enable @G }
if (auto enable = dyn_cast<EnableOp>(parent)) {
emitEnable(enable);
// Early return since an EnableOp has no body.
return;
}
// Attribute dictionary is always prepended for a control operation.
auto prependAttributes = [&](Operation *op, StringRef sym) {
return (getAttributes(op, /*atFormat=*/true) + sym).str();
};
for (auto &&op : *body) {
TypeSwitch<Operation *>(&op)
.Case<SeqOp>([&](auto op) {
emitCalyxSection(prependAttributes(op, "seq"),
[&]() { emitCalyxControl(op.getBodyBlock()); });
})
.Case<StaticSeqOp>([&](auto op) {
emitCalyxSection(prependAttributes(op, "static seq"),
[&]() { emitCalyxControl(op.getBodyBlock()); });
})
.Case<ParOp>([&](auto op) {
emitCalyxSection(prependAttributes(op, "par"),
[&]() { emitCalyxControl(op.getBodyBlock()); });
})
.Case<WhileOp>([&](auto op) {
indent() << prependAttributes(op, "while ");
emitValue(op.getCond(), /*isIndented=*/false);
if (auto groupName = op.getGroupName())
os << " with " << *groupName;
emitCalyxBody([&]() { emitCalyxControl(op.getBodyBlock()); });
})
.Case<IfOp>([&](auto op) {
indent() << prependAttributes(op, "if ");
emitValue(op.getCond(), /*isIndented=*/false);
if (auto groupName = op.getGroupName())
os << " with " << *groupName;
emitCalyxBody([&]() { emitCalyxControl(op.getThenBody()); });
if (op.elseBodyExists())
emitCalyxSection("else",
[&]() { emitCalyxControl(op.getElseBody()); });
})
.Case<StaticIfOp>([&](auto op) {
indent() << prependAttributes(op, "static if ");
emitValue(op.getCond(), /*isIndented=*/false);
emitCalyxBody([&]() { emitCalyxControl(op.getThenBody()); });
if (op.elseBodyExists())
emitCalyxSection("else",
[&]() { emitCalyxControl(op.getElseBody()); });
})
.Case<RepeatOp>([&](auto op) {
indent() << prependAttributes(op, "repeat ");
os << op.getCount();
emitCalyxBody([&]() { emitCalyxControl(op.getBodyBlock()); });
})
.Case<StaticRepeatOp>([&](auto op) {
indent() << prependAttributes(op, "static repeat ");
os << op.getCount();
emitCalyxBody([&]() { emitCalyxControl(op.getBodyBlock()); });
})
.Case<StaticParOp>([&](auto op) {
emitCalyxSection(prependAttributes(op, "static par"),
[&]() { emitCalyxControl(op.getBodyBlock()); });
})
.Case<EnableOp>([&](auto op) { emitEnable(op); })
.Case<InvokeOp>([&](auto op) { emitInvoke(op); })
.Default([&](auto op) {
emitOpError(op, "not supported for emission inside control.");
});
}
}
/// The stream we are emitting into.
llvm::raw_ostream &os;
/// Whether we have encountered any errors during emission.
bool encounteredError = false;
/// Current level of indentation. See `indent()` and
/// `addIndent()`/`reduceIndent()`.
unsigned currentIndent = 0;
};
} // end anonymous namespace
LogicalResult Emitter::finalize() { return failure(encounteredError); }
/// Emit an entire program.
void Emitter::emitModule(ModuleOp op) {
for (auto &bodyOp : *op.getBody()) {
if (auto componentOp = dyn_cast<ComponentInterface>(bodyOp))
emitComponent(componentOp);
else if (auto hwModuleExternOp = dyn_cast<hw::HWModuleExternOp>(bodyOp))
emitPrimitiveExtern(hwModuleExternOp);
else
emitOpError(&bodyOp, "Unexpected op");
}
}
/// Emit a component.
void Emitter::emitComponent(ComponentInterface op) {
std::string combinationalPrefix = op.isComb() ? "comb " : "";
indent() << combinationalPrefix << "component " << op.getName()
<< getAttributes(op, /*atFormat=*/false, nullptr);
// Emit the ports.
emitComponentPorts(op);
os << space() << LBraceEndL();
addIndent();
WiresOp wires;
ControlOp control;
// Emit cells.
emitCalyxSection("cells", [&]() {
for (auto &&bodyOp : *op.getBodyBlock()) {
TypeSwitch<Operation *>(&bodyOp)
.Case<UndefLibOp>([&](auto op) { emitUndef(op); })
.Case<WiresOp>([&](auto op) { wires = op; })
.Case<ControlOp>([&](auto op) { control = op; })
.Case<InstanceOp>([&](auto op) { emitInstance(op); })
.Case<PrimitiveOp>([&](auto op) { emitPrimitive(op); })
.Case<RegisterOp>([&](auto op) { emitRegister(op); })
.Case<MemoryOp>([&](auto op) { emitMemory(op); })
.Case<SeqMemoryOp>([&](auto op) { emitSeqMemory(op); })
.Case<hw::ConstantOp>([&](auto op) { /*Do nothing*/ })
.Case<calyx::ConstantOp>([&](auto op) { emitConstant(op); })
.Case<SliceLibOp, PadLibOp, ExtSILibOp>(
[&](auto op) { emitLibraryPrimTypedByAllPorts(op); })
.Case<LtLibOp, GtLibOp, EqLibOp, NeqLibOp, GeLibOp, LeLibOp, SltLibOp,
SgtLibOp, SeqLibOp, SneqLibOp, SgeLibOp, SleLibOp, AddLibOp,
SubLibOp, ShruLibOp, RshLibOp, SrshLibOp, LshLibOp, AndLibOp,
NotLibOp, OrLibOp, XorLibOp, WireLibOp>(
[&](auto op) { emitLibraryPrimTypedByFirstInputPort(op); })
.Case<MuxLibOp>(
[&](auto op) { emitLibraryPrimTypedByFirstOutputPort(op); })
.Case<MultPipeLibOp>(
[&](auto op) { emitLibraryPrimTypedByFirstOutputPort(op); })
.Case<RemUPipeLibOp, DivUPipeLibOp>([&](auto op) {
emitLibraryPrimTypedByFirstOutputPort(
op, /*calyxLibName=*/{"std_div_pipe"});
})
.Case<RemSPipeLibOp, DivSPipeLibOp>([&](auto op) {
emitLibraryPrimTypedByFirstOutputPort(
op, /*calyxLibName=*/{"std_sdiv_pipe"});
})
.Case<AddFOpIEEE754, MulFOpIEEE754, CompareFOpIEEE754,
FpToIntOpIEEE754, IntToFpOpIEEE754, DivSqrtOpIEEE754>(
[&](auto op) { emitLibraryFloatingPoint(op); })
.Default([&](auto op) {
emitOpError(op, "not supported for emission inside component");
});
}
});
emitWires(wires);
emitControl(control);
reduceIndent();
os << RBraceEndL();
}
/// Emit the ports of a component.
void Emitter::emitComponentPorts(ComponentInterface op) {
auto emitPorts = [&](auto ports) {
os << LParen();
for (size_t i = 0, e = ports.size(); i < e; ++i) {
const PortInfo &port = ports[i];
// We only care about the bit width in the emitted .futil file.
unsigned int bitWidth = port.type.getIntOrFloatBitWidth();
os << getAttributes(op, /*atFormat=*/true, port.attributes)
<< port.name.getValue() << colon() << bitWidth;
if (i + 1 < e)
os << comma();
}
os << RParen();
};
emitPorts(op.getInputPortInfo());
os << arrow();
emitPorts(op.getOutputPortInfo());
}
/// Emit a primitive extern
void Emitter::emitPrimitiveExtern(hw::HWModuleExternOp op) {
Attribute filename = op->getAttrDictionary().get("filename");
indent() << "extern " << filename << space() << LBraceEndL();
addIndent();
indent() << "primitive " << op.getName();
if (!op.getParameters().empty()) {
os << LSquare();
llvm::interleaveComma(op.getParameters(), os, [&](Attribute param) {
auto paramAttr = cast<hw::ParamDeclAttr>(param);
os << paramAttr.getName().str();
});
os << RSquare();
}
os << getAttributes(op, /*atFormat=*/false);
// Emit the ports.
emitPrimitivePorts(op);
os << semicolonEndL();
reduceIndent();
os << RBraceEndL();
}
/// Emit the ports of a component.
void Emitter::emitPrimitivePorts(hw::HWModuleExternOp op) {
auto emitPorts = [&](auto ports, bool isInput) {
auto e = static_cast<size_t>(std::distance(ports.begin(), ports.end()));
os << LParen();
auto type = op.getHWModuleType();
for (auto [i, port] : llvm::enumerate(ports)) {
DictionaryAttr portAttr = cast_or_null<DictionaryAttr>(
op.getPortAttrs(isInput ? type.getPortIdForInputId(i)
: type.getPortIdForOutputId(i)));
os << getAttributes(op, /*atFormat=*/true, portAttr)
<< port.name.getValue() << colon();
// We only care about the bit width in the emitted .futil file.
// Emit parameterized or non-parameterized bit width.
if (hw::isParametricType(port.type)) {
hw::ParamDeclRefAttr bitWidth = dyn_cast<hw::ParamDeclRefAttr>(
cast<hw::IntType>(port.type).getWidth());
os << bitWidth.getName().str();
} else {
unsigned int bitWidth = port.type.getIntOrFloatBitWidth();
os << bitWidth;
}
if (i < e - 1)
os << comma();
}
os << RParen();
};
hw::ModulePortInfo ports(op.getPortList());
emitPorts(ports.getInputs(), true);
os << arrow();
emitPorts(ports.getOutputs(), false);
}
void Emitter::emitInstance(InstanceOp op) {
indent() << getAttributes(op, /*atFormat=*/true) << op.instanceName()
<< space() << equals() << space() << op.getComponentName()
<< LParen() << RParen() << semicolonEndL();
}
void Emitter::emitPrimitive(PrimitiveOp op) {
indent() << getAttributes(op, /*atFormat=*/true) << op.instanceName()
<< space() << equals() << space() << op.getPrimitiveName()
<< LParen();
if (op.getParameters().has_value()) {
llvm::interleaveComma(*op.getParameters(), os, [&](Attribute param) {
auto paramAttr = cast<hw::ParamDeclAttr>(param);
auto value = paramAttr.getValue();
if (auto intAttr = dyn_cast<IntegerAttr>(value)) {
os << intAttr.getInt();
} else if (auto fpAttr = dyn_cast<FloatAttr>(value)) {
os << fpAttr.getValue().convertToFloat();
} else {
llvm_unreachable("Primitive parameter type not supported");
}
});
}
os << RParen() << semicolonEndL();
}
void Emitter::emitRegister(RegisterOp reg) {
size_t bitWidth = reg.getIn().getType().getIntOrFloatBitWidth();
indent() << getAttributes(reg, /*atFormat=*/true) << reg.instanceName()
<< space() << equals() << space() << "std_reg" << LParen()
<< std::to_string(bitWidth) << RParen() << semicolonEndL();
}
void Emitter::emitUndef(UndefLibOp op) {
size_t bitwidth = op.getOut().getType().getIntOrFloatBitWidth();
indent() << getAttributes(op, /*atFormat=*/true) << op.instanceName()
<< space() << equals() << space() << "undef" << LParen()
<< std::to_string(bitwidth) << RParen() << semicolonEndL();
}
void Emitter::emitMemory(MemoryOp memory) {
size_t dimension = memory.getSizes().size();
if (dimension < 1 || dimension > 4) {
emitOpError(memory, "Only memories with dimensionality in range [1, 4] are "
"supported by the native Calyx compiler.");
return;
}
indent() << getAttributes(memory, /*atFormat=*/true) << memory.instanceName()
<< space() << equals() << space() << "std_mem_d"
<< std::to_string(dimension) << LParen() << memory.getWidth()
<< comma();
for (Attribute size : memory.getSizes()) {
APInt memSize = cast<IntegerAttr>(size).getValue();
memSize.print(os, /*isSigned=*/false);
os << comma();
}
ArrayAttr addrSizes = memory.getAddrSizes();
for (size_t i = 0, e = addrSizes.size(); i != e; ++i) {
APInt addrSize = cast<IntegerAttr>(addrSizes[i]).getValue();
addrSize.print(os, /*isSigned=*/false);
if (i + 1 == e)
continue;
os << comma();
}
os << RParen() << semicolonEndL();
}
void Emitter::emitSeqMemory(SeqMemoryOp memory) {
size_t dimension = memory.getSizes().size();
if (dimension < 1 || dimension > 4) {
emitOpError(memory, "Only memories with dimensionality in range [1, 4] are "
"supported by the native Calyx compiler.");
return;
}
bool isRef = !memory->hasAttr("external");
indent();
if (isRef)
os << "ref ";
os << getAttributes(memory, /*atFormat=*/true) << memory.instanceName()
<< space() << equals() << space() << "seq_mem_d"
<< std::to_string(dimension) << LParen() << memory.getWidth() << comma();
for (Attribute size : memory.getSizes()) {
APInt memSize = cast<IntegerAttr>(size).getValue();
memSize.print(os, /*isSigned=*/false);
os << comma();
}
ArrayAttr addrSizes = memory.getAddrSizes();
for (size_t i = 0, e = addrSizes.size(); i != e; ++i) {
APInt addrSize = cast<IntegerAttr>(addrSizes[i]).getValue();
addrSize.print(os, /*isSigned=*/false);
if (i + 1 == e)
continue;
os << comma();
}
os << RParen() << semicolonEndL();
}
void Emitter::emitInvoke(InvokeOp invoke) {
StringRef callee = invoke.getCallee();
indent() << "invoke " << callee;
auto refCellsMap = invoke.getRefCellsMap();
if (!refCellsMap.empty()) {
os << "[";
llvm::interleaveComma(refCellsMap, os, [&](Attribute attr) {
auto dictAttr = cast<DictionaryAttr>(attr);
llvm::interleaveComma(dictAttr, os, [&](NamedAttribute namedAttr) {
auto refCellName = namedAttr.getName().str();
auto externalMem =
cast<FlatSymbolRefAttr>(namedAttr.getValue()).getValue();
os << refCellName << " = " << externalMem;
});
});
os << "]";
}
ArrayAttr portNames = invoke.getPortNames();
ArrayAttr inputNames = invoke.getInputNames();
/// Because the ports of all components of calyx.invoke are inside a (),
/// here the input and output ports are divided, inputs and outputs store
/// the connections for a subset of input and output ports of the instance.
llvm::StringMap<std::string> inputsMap;
llvm::StringMap<std::string> outputsMap;
for (auto [portNameAttr, inputNameAttr, input] :
llvm::zip(portNames, inputNames, invoke.getInputs())) {
StringRef portName = cast<StringAttr>(portNameAttr).getValue();
StringRef inputName = cast<StringAttr>(inputNameAttr).getValue();
/// Classify the connection of ports,here's an example. calyx.invoke
/// @r(%r.in = %id.out, %out = %r.out) -> (i32, i32) %r.in = %id.out will be
/// stored in inputs, because %.r.in is the input port of the component, and
/// %out = %r.out will be stored in outputs, because %r.out is the output
/// port of the component, which is a bit different from calyx's native
/// compiler. Later on, the classified connection relations are outputted
/// uniformly and converted to calyx's native compiler format.
StringRef inputMapKey = portName.drop_front(2 + callee.size());
if (portName.substr(1, callee.size()) == callee) {
// If the input to the port is a number.
if (isa_and_nonnull<hw::ConstantOp>(input.getDefiningOp())) {
hw::ConstantOp constant = cast<hw::ConstantOp>(input.getDefiningOp());
APInt value = constant.getValue();
std::string mapValue = std::to_string(value.getBitWidth()) +
apostrophe().data() + "d" +
std::to_string(value.getZExtValue());
inputsMap[inputMapKey] = mapValue;
continue;
}
inputsMap[inputMapKey] = inputName.drop_front(1).str();
} else if (inputName.substr(1, callee.size()) == callee)
outputsMap[inputName.drop_front(2 + callee.size())] =
portName.drop_front(1).str();
}
/// Emit inputs
os << LParen();
llvm::interleaveComma(inputsMap, os, [&](const auto &iter) {
os << iter.getKey() << " = " << iter.getValue();
});
os << RParen();
/// Emit outputs
os << LParen();
llvm::interleaveComma(outputsMap, os, [&](const auto &iter) {
os << iter.getKey() << " = " << iter.getValue();
});
os << RParen() << semicolonEndL();
}
void Emitter::emitConstant(ConstantOp constantOp) {
TypedAttr attr = constantOp.getValueAttr();
assert(isa<FloatAttr>(attr) && "must be a floating point constant");
auto fltAttr = cast<FloatAttr>(attr);
APFloat value = fltAttr.getValue();
auto type = cast<FloatType>(fltAttr.getType());
double doubleValue = value.convertToDouble();
auto floatBits = value.getSizeInBits(type.getFloatSemantics());
indent() << constantOp.getName().str() << space() << equals() << space()
<< "std_float_const";
// Currently defaults to IEEE-754 representation [1].
// [1]: https://github.com/calyxir/calyx/blob/main/primitives/float.futil
static constexpr int32_t IEEE754 = 0;
os << LParen() << std::to_string(IEEE754) << comma() << floatBits << comma()
<< std::to_string(doubleValue) << RParen() << semicolonEndL();
}
/// Calling getName() on a calyx operation will return "calyx.${opname}". This
/// function returns whatever is left after the first '.' in the string,
/// removing the 'calyx' prefix.
static StringRef removeCalyxPrefix(StringRef s) { return s.split(".").second; }
void Emitter::emitLibraryPrimTypedByAllPorts(Operation *op) {
auto cell = cast<CellInterface>(op);
indent() << getAttributes(op, /*atFormat=*/true) << cell.instanceName()
<< space() << equals() << space()
<< removeCalyxPrefix(op->getName().getStringRef()) << LParen();
llvm::interleaveComma(op->getResults(), os, [&](auto res) {
os << std::to_string(res.getType().getIntOrFloatBitWidth());
});
os << RParen() << semicolonEndL();
}
void Emitter::emitLibraryPrimTypedByFirstInputPort(Operation *op) {
auto cell = cast<CellInterface>(op);
unsigned bitWidth = cell.getInputPorts()[0].getType().getIntOrFloatBitWidth();
StringRef opName = op->getName().getStringRef();
indent() << getAttributes(op, /*atFormat=*/true) << cell.instanceName()
<< space() << equals() << space() << removeCalyxPrefix(opName)
<< LParen() << bitWidth << RParen() << semicolonEndL();
}
void Emitter::emitLibraryPrimTypedByFirstOutputPort(
Operation *op, std::optional<StringRef> calyxLibName) {
auto cell = cast<CellInterface>(op);
unsigned bitWidth =
cell.getOutputPorts()[0].getType().getIntOrFloatBitWidth();
StringRef opName = op->getName().getStringRef();
indent() << getAttributes(op, /*atFormat=*/true) << cell.instanceName()
<< space() << equals() << space()
<< (calyxLibName ? *calyxLibName : removeCalyxPrefix(opName))
<< LParen() << bitWidth << RParen() << semicolonEndL();
}
unsigned getFPBitWidth(CellInterface &cell) {
if (isa<IntToFpOpIEEE754>(cell.getOperation())) {
// `std_intToFp` has the float point value in the first output port.
return cell.getOutputPorts()[0].getType().getIntOrFloatBitWidth();
}
auto inputPorts = cell.getInputPorts();
assert(inputPorts.size() >= 2 && "There should be at least two input ports");
// magic number for the index of `left/right` input port for
// `AddF`/`MulF`/`CompareF`; `in` input port for `FpToInt`.
size_t inputPortIndex = inputPorts.size() - 2;
return cell.getInputPorts()[inputPortIndex].getType().getIntOrFloatBitWidth();
}
unsigned getIntWidth(Operation *op, CellInterface &cell, bool isIntToFp) {
auto inputPorts = cell.getInputPorts();
assert(inputPorts.size() >= 2);
size_t integerIndex = inputPorts.size() - 2;
if (!isIntToFp) {
// FpToInt case: output width determines integer width.
return cell.getOutputPorts()[0].getType().getIntOrFloatBitWidth();
}
return cell.getInputPorts()[integerIndex].getType().getIntOrFloatBitWidth();
}
void Emitter::emitLibraryFloatingPoint(Operation *op) {
auto cell = cast<CellInterface>(op);
unsigned fpBitWidth = getFPBitWidth(cell);
unsigned expWidth, sigWidth;
switch (fpBitWidth) {
case 16:
expWidth = 5;
sigWidth = 11;
break;
case 32:
expWidth = 8;
sigWidth = 24;
break;
case 64:
expWidth = 11;
sigWidth = 53;
break;
case 128:
expWidth = 15;
sigWidth = 113;
break;
default:
op->emitError("The supported bitwidths are 16, 32, 64, and 128");
return;
}
std::string opName;
if (auto fpOp = dyn_cast<calyx::FloatingPointOpInterface>(op)) {
opName = fpOp.getCalyxLibraryName();
}
indent() << getAttributes(op, /*atFormat=*/true) << cell.instanceName()
<< space() << equals() << space() << opName << LParen();
if (isa<calyx::IntToFpOpIEEE754>(op)) {
unsigned intWidth = getIntWidth(op, cell, /*isIntToFp=*/true);
os << intWidth << comma();
}
os << expWidth << comma() << sigWidth << comma() << fpBitWidth;
if (auto fpToIntOp = dyn_cast<calyx::FpToIntOpIEEE754>(op)) {
unsigned intWidth = getIntWidth(op, cell, /*isIntToFp=*/false);
os << comma() << intWidth;
}
os << RParen() << semicolonEndL();
}
void Emitter::emitAssignment(AssignOp op) {
emitValue(op.getDest(), /*isIndented=*/true);
os << space() << equals() << space();
if (op.getGuard()) {
emitValue(op.getGuard(), /*isIndented=*/false);
os << questionMark();
}
if (auto constantOp =
dyn_cast_or_null<calyx::ConstantOp>(op.getSrc().getDefiningOp())) {
TypedAttr attr = constantOp.getValueAttr();
assert(isa<FloatAttr>(attr) && "must be a floating point constant");
auto fltAttr = dyn_cast<FloatAttr>(attr);
assert(attr != nullptr && "must be a floating point constant");
APFloat value = fltAttr.getValue();
if (value.isInfinity() || value.isNaN() || value.isNegative()) {
SmallString<8> str;
auto intValue = value.bitcastToAPInt();
intValue.toStringUnsigned(str, /*Radix=*/2);
os << std::to_string(intValue.getBitWidth()) << apostrophe() << "b" << str
<< semicolonEndL();
return;
}
}
emitValue(op.getSrc(), /*isIndented=*/false);
os << semicolonEndL();
}
void Emitter::emitWires(WiresOp op) {
emitCalyxSection("wires", [&]() {
for (auto &&bodyOp : *op.getBodyBlock()) {
TypeSwitch<Operation *>(&bodyOp)
.Case<GroupInterface>([&](auto op) { emitGroup(op); })
.Case<AssignOp>([&](auto op) { emitAssignment(op); })
.Case<hw::ConstantOp, calyx::ConstantOp, comb::AndOp, comb::OrOp,
comb::XorOp, CycleOp>([&](auto op) { /* Do nothing. */ })
.Default([&](auto op) {
emitOpError(op, "not supported for emission inside wires section");
});
}
});
}
void Emitter::emitGroup(GroupInterface group) {
auto emitGroupBody = [&]() {
for (auto &&bodyOp : *group.getBody()) {
TypeSwitch<Operation *>(&bodyOp)
.Case<AssignOp>([&](auto op) { emitAssignment(op); })
.Case<GroupDoneOp>([&](auto op) { emitGroupPort(group, op, "done"); })
.Case<GroupGoOp>([&](auto op) { emitGroupPort(group, op, "go"); })
.Case<hw::ConstantOp, comb::AndOp, comb::OrOp, comb::XorOp, CycleOp>(
[&](auto op) { /* Do nothing. */ })
.Default([&](auto op) {
emitOpError(op, "not supported for emission inside group.");
});
}
};
std::string prefix;
if (isa<StaticGroupOp>(group)) {
auto staticGroup = cast<StaticGroupOp>(group);
prefix = llvm::formatv("static<{0}> group", staticGroup.getLatency());
} else {
prefix = isa<CombGroupOp>(group) ? "comb group" : "group";
}
auto groupHeader =
(group.symName().getValue() + getAttributes(group, /*atFormat=*/false))
.str();
emitCalyxSection(prefix, emitGroupBody, groupHeader);
}
void Emitter::emitEnable(EnableOp enable) {
indent() << getAttributes(enable, /*atFormat=*/true) << enable.getGroupName()
<< semicolonEndL();
}
void Emitter::emitControl(ControlOp control) {
// A valid Calyx program does not necessarily need a control section.
if (control == nullptr)
return;
emitCalyxSection("control",
[&]() { emitCalyxControl(control.getBodyBlock()); });
}
//===----------------------------------------------------------------------===//
// Driver
//===----------------------------------------------------------------------===//
// Emit the specified Calyx circuit into the given output stream.
mlir::LogicalResult circt::calyx::exportCalyx(mlir::ModuleOp module,
llvm::raw_ostream &os) {
Emitter emitter(os);
if (failed(emitter.emitImports(module)))
return failure();
emitter.emitModule(module);
emitter.emitCiderMetadata(module);
return emitter.finalize();
}
void circt::calyx::registerToCalyxTranslation() {
static mlir::TranslateFromMLIRRegistration toCalyx(
"export-calyx", "export Calyx",
[](ModuleOp module, llvm::raw_ostream &os) {
return exportCalyx(module, os);
},
[](mlir::DialectRegistry &registry) {
registry
.insert<calyx::CalyxDialect, comb::CombDialect, hw::HWDialect>();
});
}