mirror of https://github.com/llvm/circt.git
[ImportVerilog] Add basic function support (#7349)
Add basic support for function definitions and calls to such functions. Input function arguments are currently passed into the function by value, while output, inout, ref, and const ref arguments are passed by reference at the moment. Later on we may want to be more faithful to the SV spec and make output and inout arguments also pass by value. Calls to void functions are slightly strange in that they don't really have a result value, but the `convertExpression` function really wants a valid `Value` to return. To work around this, these calls generate an additional `!moore.void` value using `unrealized_conversion_cast`, which gets deleted again by the statement conversion code.
This commit is contained in:
parent
414bbb7d9b
commit
1345859484
|
@ -42,6 +42,7 @@ add_circt_translation_library(CIRCTImportVerilog
|
|||
LINK_LIBS PUBLIC
|
||||
CIRCTHW
|
||||
CIRCTMoore
|
||||
MLIRFuncDialect
|
||||
MLIRSCFDialect
|
||||
MLIRTranslateLib
|
||||
PRIVATE
|
||||
|
|
|
@ -627,8 +627,47 @@ struct RvalueExprVisitor {
|
|||
/// Handle subroutine calls.
|
||||
Value visitCall(const slang::ast::CallExpression &expr,
|
||||
const slang::ast::SubroutineSymbol *subroutine) {
|
||||
mlir::emitError(loc, "unsupported subroutine call");
|
||||
return {};
|
||||
auto *lowering = context.declareFunction(*subroutine);
|
||||
if (!lowering)
|
||||
return {};
|
||||
|
||||
// Convert the call arguments. Input arguments are converted to an rvalue.
|
||||
// All other arguments are converted to lvalues and passed into the function
|
||||
// by reference.
|
||||
SmallVector<Value> arguments;
|
||||
for (auto [callArg, declArg] :
|
||||
llvm::zip(expr.arguments(), subroutine->getArguments())) {
|
||||
|
||||
// Unpack the `<expr> = EmptyArgument` pattern emitted by Slang for output
|
||||
// and inout arguments.
|
||||
auto *expr = callArg;
|
||||
if (const auto *assign = expr->as_if<slang::ast::AssignmentExpression>())
|
||||
expr = &assign->left();
|
||||
|
||||
Value value;
|
||||
if (declArg->direction == slang::ast::ArgumentDirection::In)
|
||||
value = context.convertRvalueExpression(*expr);
|
||||
else
|
||||
value = context.convertLvalueExpression(*expr);
|
||||
if (!value)
|
||||
return {};
|
||||
arguments.push_back(value);
|
||||
}
|
||||
|
||||
// Create the call.
|
||||
auto callOp =
|
||||
builder.create<mlir::func::CallOp>(loc, lowering->op, arguments);
|
||||
|
||||
// For calls to void functions we need to have a value to return from this
|
||||
// function. Create a dummy `unrealized_conversion_cast`, which will get
|
||||
// deleted again later on.
|
||||
if (callOp.getNumResults() == 0)
|
||||
return builder
|
||||
.create<mlir::UnrealizedConversionCastOp>(
|
||||
loc, moore::VoidType::get(context.getContext()), ValueRange{})
|
||||
.getResult(0);
|
||||
|
||||
return callOp.getResult(0);
|
||||
}
|
||||
|
||||
/// Handle system calls.
|
||||
|
|
|
@ -259,8 +259,8 @@ LogicalResult ImportDriver::importVerilog(ModuleOp module) {
|
|||
compileTimer.stop();
|
||||
|
||||
// Traverse the parsed Verilog AST and map it to the equivalent CIRCT ops.
|
||||
mlirContext
|
||||
->loadDialect<moore::MooreDialect, hw::HWDialect, scf::SCFDialect>();
|
||||
mlirContext->loadDialect<moore::MooreDialect, hw::HWDialect, scf::SCFDialect,
|
||||
func::FuncDialect>();
|
||||
auto conversionTimer = ts.nest("Verilog to dialect mapping");
|
||||
Context context(*compilation, module, driver.sourceManager, bufferFilePaths);
|
||||
if (failed(context.convertCompilation()))
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#include "circt/Conversion/ImportVerilog.h"
|
||||
#include "circt/Dialect/HW/HWOps.h"
|
||||
#include "circt/Dialect/Moore/MooreOps.h"
|
||||
#include "mlir/Dialect/Func/IR/FuncOps.h"
|
||||
#include "mlir/Dialect/SCF/IR/SCF.h"
|
||||
#include "slang/ast/ASTVisitor.h"
|
||||
#include "llvm/ADT/ScopedHashTable.h"
|
||||
|
@ -40,6 +41,11 @@ struct ModuleLowering {
|
|||
portsBySyntaxNode;
|
||||
};
|
||||
|
||||
/// Function lowering information.
|
||||
struct FunctionLowering {
|
||||
mlir::func::FuncOp op;
|
||||
};
|
||||
|
||||
/// A helper class to facilitate the conversion from a Slang AST to MLIR
|
||||
/// operations. Keeps track of the destination MLIR module, builders, and
|
||||
/// various worklists and utilities needed for conversion.
|
||||
|
@ -74,6 +80,9 @@ struct Context {
|
|||
convertModuleHeader(const slang::ast::InstanceBodySymbol *module);
|
||||
LogicalResult convertModuleBody(const slang::ast::InstanceBodySymbol *module);
|
||||
LogicalResult convertPackage(const slang::ast::PackageSymbol &package);
|
||||
FunctionLowering *
|
||||
declareFunction(const slang::ast::SubroutineSymbol &subroutine);
|
||||
LogicalResult convertFunction(const slang::ast::SubroutineSymbol &subroutine);
|
||||
|
||||
// Convert a statement AST node to MLIR ops.
|
||||
LogicalResult convertStatement(const slang::ast::Statement &stmt);
|
||||
|
@ -99,6 +108,7 @@ struct Context {
|
|||
/// The top-level operations ordered by their Slang source location. This is
|
||||
/// used to produce IR that follows the source file order.
|
||||
std::map<slang::SourceLocation, Operation *> orderedRootOps;
|
||||
|
||||
/// How we have lowered modules to MLIR.
|
||||
DenseMap<const slang::ast::InstanceBodySymbol *,
|
||||
std::unique_ptr<ModuleLowering>>
|
||||
|
@ -107,6 +117,11 @@ struct Context {
|
|||
/// not been converted yet.
|
||||
std::queue<const slang::ast::InstanceBodySymbol *> moduleWorklist;
|
||||
|
||||
/// Functions that have already been converted.
|
||||
DenseMap<const slang::ast::SubroutineSymbol *,
|
||||
std::unique_ptr<FunctionLowering>>
|
||||
functions;
|
||||
|
||||
/// A table of defined values, such as variables, that may be referred to by
|
||||
/// name in expressions. The expressions use this table to lookup the MLIR
|
||||
/// value that was created for a given declaration in the Slang AST node.
|
||||
|
|
|
@ -46,7 +46,17 @@ struct StmtVisitor {
|
|||
|
||||
// Handle expression statements.
|
||||
LogicalResult visit(const slang::ast::ExpressionStatement &stmt) {
|
||||
return failure(!context.convertRvalueExpression(stmt.expr));
|
||||
auto value = context.convertRvalueExpression(stmt.expr);
|
||||
if (!value)
|
||||
return failure();
|
||||
|
||||
// Expressions like calls to void functions return a dummy value that has no
|
||||
// uses. If the returned value is trivially dead, remove it.
|
||||
if (auto *defOp = value.getDefiningOp())
|
||||
if (isOpTriviallyDead(defOp))
|
||||
defOp->erase();
|
||||
|
||||
return success();
|
||||
}
|
||||
|
||||
// Handle variable declarations.
|
||||
|
@ -317,6 +327,18 @@ struct StmtVisitor {
|
|||
return success();
|
||||
}
|
||||
|
||||
// Handle return statements.
|
||||
LogicalResult visit(const slang::ast::ReturnStatement &stmt) {
|
||||
Value expr;
|
||||
if (stmt.expr) {
|
||||
expr = context.convertRvalueExpression(*stmt.expr);
|
||||
if (!expr)
|
||||
return failure();
|
||||
}
|
||||
builder.create<mlir::func::ReturnOp>(loc, expr);
|
||||
return success();
|
||||
}
|
||||
|
||||
/// Emit an error for all other statements.
|
||||
template <typename T>
|
||||
LogicalResult visit(T &&stmt) {
|
||||
|
|
|
@ -64,6 +64,11 @@ struct RootVisitor : public BaseVisitor {
|
|||
return context.convertPackage(package);
|
||||
}
|
||||
|
||||
// Handle functions and tasks.
|
||||
LogicalResult visit(const slang::ast::SubroutineSymbol &subroutine) {
|
||||
return context.convertFunction(subroutine);
|
||||
}
|
||||
|
||||
// Emit an error for all other members.
|
||||
template <typename T>
|
||||
LogicalResult visit(T &&node) {
|
||||
|
@ -89,6 +94,15 @@ struct PackageVisitor : public BaseVisitor {
|
|||
PackageVisitor(Context &context, Location loc)
|
||||
: context(context), loc(loc), builder(context.builder) {}
|
||||
|
||||
// Ignore parameters. These are materialized on-the-fly as `ConstantOp`s.
|
||||
LogicalResult visit(const slang::ast::ParameterSymbol &) { return success(); }
|
||||
LogicalResult visit(const slang::ast::SpecparamSymbol &) { return success(); }
|
||||
|
||||
// Handle functions and tasks.
|
||||
LogicalResult visit(const slang::ast::SubroutineSymbol &subroutine) {
|
||||
return context.convertFunction(subroutine);
|
||||
}
|
||||
|
||||
/// Emit an error for all other members.
|
||||
template <typename T>
|
||||
LogicalResult visit(T &&node) {
|
||||
|
@ -502,6 +516,11 @@ struct ModuleVisitor : public BaseVisitor {
|
|||
return success();
|
||||
}
|
||||
|
||||
// Handle functions and tasks.
|
||||
LogicalResult visit(const slang::ast::SubroutineSymbol &subroutine) {
|
||||
return context.convertFunction(subroutine);
|
||||
}
|
||||
|
||||
/// Emit an error for all other members.
|
||||
template <typename T>
|
||||
LogicalResult visit(T &&node) {
|
||||
|
@ -768,3 +787,141 @@ Context::convertPackage(const slang::ast::PackageSymbol &package) {
|
|||
}
|
||||
return success();
|
||||
}
|
||||
|
||||
static void guessNamespacePrefix(const slang::ast::Symbol &symbol,
|
||||
SmallString<64> &prefix) {
|
||||
if (symbol.kind == slang::ast::SymbolKind::Root)
|
||||
return;
|
||||
guessNamespacePrefix(symbol.getParentScope()->asSymbol(), prefix);
|
||||
if (!symbol.name.empty()) {
|
||||
prefix += symbol.name;
|
||||
prefix += "::";
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a function and its arguments to a function declaration in the IR.
|
||||
/// This does not convert the function body.
|
||||
FunctionLowering *
|
||||
Context::declareFunction(const slang::ast::SubroutineSymbol &subroutine) {
|
||||
using slang::ast::ArgumentDirection;
|
||||
|
||||
// Check if there already is a declaration for this function.
|
||||
auto &lowering = functions[&subroutine];
|
||||
if (lowering) {
|
||||
if (!lowering->op)
|
||||
return {};
|
||||
return lowering.get();
|
||||
}
|
||||
lowering = std::make_unique<FunctionLowering>();
|
||||
auto loc = convertLocation(subroutine.location);
|
||||
|
||||
// Pick an insertion point for this function according to the source file
|
||||
// location.
|
||||
OpBuilder::InsertionGuard g(builder);
|
||||
auto it = orderedRootOps.upper_bound(subroutine.location);
|
||||
if (it == orderedRootOps.end())
|
||||
builder.setInsertionPointToEnd(intoModuleOp.getBody());
|
||||
else
|
||||
builder.setInsertionPoint(it->second);
|
||||
|
||||
// Class methods are currently not supported.
|
||||
if (subroutine.thisVar) {
|
||||
mlir::emitError(loc) << "unsupported class method";
|
||||
return {};
|
||||
}
|
||||
|
||||
// Determine the function type.
|
||||
SmallVector<Type> inputTypes;
|
||||
SmallVector<Type, 1> outputTypes;
|
||||
|
||||
for (const auto *arg : subroutine.getArguments()) {
|
||||
auto type = cast<moore::UnpackedType>(convertType(arg->getType()));
|
||||
if (!type)
|
||||
return {};
|
||||
if (arg->direction == ArgumentDirection::In) {
|
||||
inputTypes.push_back(type);
|
||||
} else {
|
||||
inputTypes.push_back(moore::RefType::get(type));
|
||||
}
|
||||
}
|
||||
|
||||
if (!subroutine.getReturnType().isVoid()) {
|
||||
auto type = convertType(subroutine.getReturnType());
|
||||
if (!type)
|
||||
return {};
|
||||
outputTypes.push_back(type);
|
||||
}
|
||||
|
||||
auto funcType = FunctionType::get(getContext(), inputTypes, outputTypes);
|
||||
|
||||
// Prefix the function name with the surrounding namespace to create somewhat
|
||||
// sane names in the IR.
|
||||
SmallString<64> funcName;
|
||||
guessNamespacePrefix(subroutine.getParentScope()->asSymbol(), funcName);
|
||||
funcName += subroutine.name;
|
||||
|
||||
// Create a function declaration.
|
||||
auto funcOp = builder.create<mlir::func::FuncOp>(loc, funcName, funcType);
|
||||
SymbolTable::setSymbolVisibility(funcOp, SymbolTable::Visibility::Private);
|
||||
orderedRootOps.insert(it, {subroutine.location, funcOp});
|
||||
lowering->op = funcOp;
|
||||
|
||||
// Add the function to the symbol table of the MLIR module, which uniquifies
|
||||
// its name.
|
||||
symbolTable.insert(funcOp);
|
||||
|
||||
return lowering.get();
|
||||
}
|
||||
|
||||
/// Convert a function.
|
||||
LogicalResult
|
||||
Context::convertFunction(const slang::ast::SubroutineSymbol &subroutine) {
|
||||
// First get or create the function declaration.
|
||||
auto *lowering = declareFunction(subroutine);
|
||||
if (!lowering)
|
||||
return failure();
|
||||
ValueSymbolScope scope(valueSymbols);
|
||||
|
||||
// Create a function body block and populate it with block arguments.
|
||||
auto &block = lowering->op.getBody().emplaceBlock();
|
||||
for (auto [astArg, type] :
|
||||
llvm::zip(subroutine.getArguments(),
|
||||
lowering->op.getFunctionType().getInputs())) {
|
||||
auto loc = convertLocation(astArg->location);
|
||||
auto blockArg = block.addArgument(type, loc);
|
||||
valueSymbols.insert(astArg, blockArg);
|
||||
}
|
||||
|
||||
// Convert the body of the function.
|
||||
OpBuilder::InsertionGuard g(builder);
|
||||
builder.setInsertionPointToEnd(&block);
|
||||
|
||||
Value returnVar;
|
||||
if (subroutine.returnValVar) {
|
||||
auto type = convertType(*subroutine.returnValVar->getDeclaredType());
|
||||
if (!type)
|
||||
return failure();
|
||||
returnVar = builder.create<moore::VariableOp>(
|
||||
lowering->op.getLoc(),
|
||||
moore::RefType::get(cast<moore::UnpackedType>(type)), StringAttr{},
|
||||
Value{});
|
||||
valueSymbols.insert(subroutine.returnValVar, returnVar);
|
||||
}
|
||||
|
||||
if (failed(convertStatement(subroutine.getBody())))
|
||||
return failure();
|
||||
|
||||
// If there was no explicit return statement provided by the user, insert a
|
||||
// default one.
|
||||
if (block.empty() || !block.back().hasTrait<OpTrait::IsTerminator>()) {
|
||||
if (returnVar && !subroutine.getReturnType().isVoid()) {
|
||||
returnVar = builder.create<moore::ReadOp>(returnVar.getLoc(), returnVar);
|
||||
builder.create<mlir::func::ReturnOp>(lowering->op.getLoc(), returnVar);
|
||||
} else {
|
||||
builder.create<mlir::func::ReturnOp>(lowering->op.getLoc(), ValueRange{});
|
||||
}
|
||||
}
|
||||
if (returnVar.use_empty())
|
||||
returnVar.getDefiningOp()->erase();
|
||||
return success();
|
||||
}
|
||||
|
|
|
@ -1394,3 +1394,101 @@ endmodule
|
|||
package Package;
|
||||
typedef logic [41:0] PackageType;
|
||||
endpackage
|
||||
|
||||
// CHECK-LABEL: func.func private @simpleFunc1(
|
||||
// CHECK-SAME: %arg0: !moore.i32
|
||||
// CHECK-SAME: %arg1: !moore.i32
|
||||
// CHECK-SAME: ) -> !moore.i32
|
||||
function int simpleFunc1(int a, b);
|
||||
// CHECK: [[RETVAR:%.+]] = moore.variable : <i32>
|
||||
// CHECK: [[TMP:%.+]] = moore.add %arg0, %arg1
|
||||
// CHECK: moore.blocking_assign [[RETVAR]], [[TMP]]
|
||||
simpleFunc1 = a + b;
|
||||
// CHECK: [[TMP:%.+]] = moore.read [[RETVAR]]
|
||||
// CHECK: return [[TMP]]
|
||||
endfunction
|
||||
|
||||
// CHECK-LABEL: func.func private @simpleFunc2(
|
||||
// CHECK-SAME: %arg0: !moore.i32
|
||||
// CHECK-SAME: %arg1: !moore.i32
|
||||
// CHECK-SAME: ) -> !moore.i32
|
||||
function int simpleFunc2(int a, b);
|
||||
// CHECK: [[TMP:%.+]] = moore.add %arg0, %arg1
|
||||
// CHECK: return [[TMP]]
|
||||
return a + b;
|
||||
endfunction
|
||||
|
||||
package FuncPackage;
|
||||
// CHECK-LABEL: func.func private @"FuncPackage::simpleFunc3"(
|
||||
// CHECK-SAME: %arg0: !moore.i32
|
||||
// CHECK-SAME: %arg1: !moore.i32
|
||||
// CHECK-SAME: ) -> !moore.i32
|
||||
function int simpleFunc3(int a, b);
|
||||
// CHECK: [[TMP:%.+]] = moore.mul %arg0, %arg1
|
||||
// CHECK: return [[TMP]]
|
||||
return a * b;
|
||||
endfunction
|
||||
endpackage
|
||||
|
||||
// CHECK-LABEL: func.func private @simpleFunc4(
|
||||
// CHECK-SAME: %arg0: !moore.i32
|
||||
// CHECK-SAME: %arg1: !moore.i32
|
||||
// CHECK-SAME: )
|
||||
function void simpleFunc4(int a, b);
|
||||
// CHECK: [[TMP1:%.+]] = call @simpleFunc1(%arg0, %arg1)
|
||||
// CHECK: [[TMP2:%.+]] = call @simpleFunc2(%arg0, %arg1)
|
||||
// CHECK: {{%.+}} = call @"FuncPackage::simpleFunc3"([[TMP1]], [[TMP2]])
|
||||
FuncPackage::simpleFunc3(
|
||||
simpleFunc1(a, b),
|
||||
simpleFunc2(a, b)
|
||||
);
|
||||
// CHECK: return
|
||||
endfunction
|
||||
|
||||
// CHECK-LABEL: func.func private @simpleFunc5()
|
||||
function void simpleFunc5();
|
||||
// CHECK: [[TMP1:%.+]] = moore.constant 42 : i32
|
||||
// CHECK: [[TMP2:%.+]] = moore.constant 9001 : i32
|
||||
// CHECK: call @simpleFunc4([[TMP1]], [[TMP2]])
|
||||
simpleFunc4(42, 9001);
|
||||
// CHECK: return
|
||||
endfunction
|
||||
|
||||
// CHECK-LABEL: func.func private @funcArgs1(
|
||||
// CHECK-SAME: %arg0: !moore.i32
|
||||
// CHECK-SAME: %arg1: !moore.ref<i32>
|
||||
// CHECK-SAME: %arg2: !moore.ref<i32>
|
||||
// CHECK-SAME: %arg3: !moore.ref<i32>
|
||||
// CHECK-SAME: %arg4: !moore.ref<i32>
|
||||
// CHECK-SAME: )
|
||||
function automatic void funcArgs1(
|
||||
input int a,
|
||||
output int b,
|
||||
inout int c,
|
||||
ref int d,
|
||||
const ref int e
|
||||
);
|
||||
// CHECK: moore.blocking_assign %arg1, %arg0
|
||||
b = a;
|
||||
// CHECK: [[TMP1:%.+]] = moore.read %arg2
|
||||
// CHECK: [[TMP2:%.+]] = moore.add [[TMP1]], %arg0
|
||||
// CHECK: moore.blocking_assign %arg2, [[TMP2]]
|
||||
c += a;
|
||||
// CHECK: [[TMP:%.+]] = moore.read %arg4
|
||||
// CHECK: moore.blocking_assign %arg3, [[TMP]]
|
||||
d = e;
|
||||
// CHECK: return
|
||||
endfunction
|
||||
|
||||
// CHECK-LABEL: func.func private @funcArgs2()
|
||||
function void funcArgs2();
|
||||
// CHECK: %x = moore.variable
|
||||
// CHECK: %y = moore.variable
|
||||
// CHECK: %z = moore.variable
|
||||
// CHECK: %w = moore.variable
|
||||
int x, y, z, w;
|
||||
// CHECK: [[TMP:%.+]] = moore.constant 42
|
||||
// CHECK: call @funcArgs1([[TMP]], %x, %y, %z, %w)
|
||||
funcArgs1(42, x, y, z, w);
|
||||
// CHECK: return
|
||||
endfunction
|
||||
|
|
Loading…
Reference in New Issue