[ImportVerilog] Convert initial/always/final procedures (#6766)

Add the `moore.procedure` operation to represent `initial`, `final`,
`always`, `always_comb`, `always_latch`, and `always_ff` procedures.
Extend the ImportVerilog conversion to map the corresponding Slang AST
nodes to these new operations.

These procedures expect a valid body statement or statement block. In
order to write basic tests, and in preparation for statement conversion
in the future, add the `Statements.cpp` file with a `StmtVisitor` that
handles the conversion of Verilog statements to MLIR ops. A new
`convertStatement` function on the conversion context acts as entry
point to convert statements, similar to `convertModule*` or
`convertType`. We only support a few basic statements as of now, mainly
stray semicolons, nested blocks, and variable declarations. This commit
explicitly does not handle variable lifetime yet, which is a topic for
a future refinement PR.

Thanks @hailongSun2000 for doing the heavy lifting here and getting this
going.

Co-authored-by: Hailong Sun <hailong.sun@terapines.com>
This commit is contained in:
Fabian Schuiki 2024-02-29 09:10:07 -08:00 committed by GitHub
parent 2d588384a2
commit 0749166b43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 264 additions and 12 deletions

View File

@ -73,6 +73,69 @@ def InstanceOp : MooreOp<"instance", [
}];
}
def Initial: I32EnumAttrCase<"Initial", 0, "initial">;
def Final: I32EnumAttrCase<"Final", 1, "final">;
def Always: I32EnumAttrCase<"Always", 2, "always">;
def AlwaysComb: I32EnumAttrCase<"AlwaysComb", 3, "always_comb">;
def AlwaysLatch: I32EnumAttrCase<"AlwaysLatch", 4, "always_latch">;
def AlwaysFF: I32EnumAttrCase<"AlwaysFF", 5, "always_ff">;
def ProcedureKindAttr: I32EnumAttr<"ProcedureKind", "Procedure kind",
[Initial, Final, Always, AlwaysComb, AlwaysLatch, AlwaysFF]>{
let cppNamespace = "circt::moore";
}
def ProcedureOp : MooreOp<"procedure", [
SingleBlock,
NoTerminator,
NoRegionArguments,
RecursiveMemoryEffects,
RecursivelySpeculatable
]> {
let summary = "A procedure executed at different points in time";
let description = [{
The `moore.procedure` operation represents the SystemVerilog `initial`,
`final`, `always`, `always_comb`, `always_latch`, and `always_ff`
procedures.
Execution times of the various procedures:
- An `initial` procedure is executed once at the start of a design's
lifetime, before any other procedures are executed.
- A `final` procedure is executed once at the end of a design's lifetime,
after all other procedures have stopped execution.
- An `always` or `always_ff` procedure is repeatedly executed during a
design's lifetime. Timing and event control inside the procedure can
suspend its execution, for example to wait for a signal to change. If no
such timing or event control is present, the procedure repeats infinitely
at the current timestep, effectively deadlocking the design.
- An `always_comb` or `always_latch` procedure is executed once at the start
of a design's lifetime, after any `initial` procedures, and throughout the
lifetime of the design whenever any of the variables read by the body of
the procedure changes. Since the procedure is only executed when its
change, and not repeatedly, the body generally does not contain any timing
or event control. This behavior mitigates a shortcoming of `always`
procedures, which commonly have an event control like `@*` that blocks
and waits for a change of any input signals. This prevents the body from
executing when the design is initialized and properly reacting to the
initial values of signals. In contrast, `always_comb` and `always_latch`
procedures have an implicit unconditional execution at design start-up.
See IEEE 1800-2017 § 9.2 "Structured procedures".
}];
let regions = (region SizedRegion<1>:$bodyRegion);
let arguments = (ins ProcedureKindAttr:$kind);
let results = (outs);
let assemblyFormat = [{
$kind attr-dict-with-keyword $bodyRegion
}];
}
//===----------------------------------------------------------------------===//
// Declarations
//===----------------------------------------------------------------------===//

View File

@ -30,6 +30,7 @@ endif ()
add_circt_translation_library(CIRCTImportVerilog
ImportVerilog.cpp
Statements.cpp
Structure.cpp
Types.cpp

View File

@ -54,6 +54,9 @@ struct Context {
convertModuleHeader(const slang::ast::InstanceBodySymbol *module);
LogicalResult convertModuleBody(const slang::ast::InstanceBodySymbol *module);
// Convert a statement AST node to MLIR ops.
LogicalResult convertStatement(const slang::ast::Statement *statement);
mlir::ModuleOp intoModuleOp;
const slang::SourceManager &sourceManager;
SmallDenseMap<slang::BufferID, StringRef> &bufferFilePaths;

View File

@ -0,0 +1,90 @@
//===- Statements.cpp - Slang statement conversion ------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
#include "ImportVerilogInternals.h"
using namespace circt;
using namespace ImportVerilog;
// NOLINTBEGIN(misc-no-recursion)
namespace {
struct StmtVisitor {
Context &context;
Location loc;
OpBuilder &builder;
StmtVisitor(Context &context, Location loc)
: context(context), loc(loc), builder(context.builder) {}
// Skip empty statements (stray semicolons).
LogicalResult visit(const slang::ast::EmptyStatement &) { return success(); }
// Convert every statement in a statement list. The Verilog syntax follows a
// similar philosophy as C/C++, where things like `if` and `for` accept a
// single statement as body. But then a `{...}` block is a valid statement,
// which allows for the `if {...}` syntax. In Verilog, things like `final`
// accept a single body statement, but that can be a `begin ... end` block,
// which in turn has a single body statement, which then commonly is a list of
// statements.
LogicalResult visit(const slang::ast::StatementList &stmts) {
for (auto *stmt : stmts.list)
if (failed(stmt->visit(*this)))
return failure();
return success();
}
// Inline `begin ... end` blocks into the parent.
LogicalResult visit(const slang::ast::BlockStatement &stmt) {
return stmt.body.visit(*this);
}
// Handle variable declarations.
LogicalResult visit(const slang::ast::VariableDeclStatement &stmt) {
const auto &var = stmt.symbol;
auto type = context.convertType(*var.getDeclaredType());
if (!type)
return failure();
Value initial;
if (const auto *init = var.getInitializer()) {
return mlir::emitError(loc,
"variable initializer expressions not supported");
}
builder.create<moore::VariableOp>(loc, type,
builder.getStringAttr(var.name), initial);
return success();
}
// Ignore timing control for now.
LogicalResult visit(const slang::ast::TimedStatement &stmt) {
return success();
}
/// Emit an error for all other statements.
template <typename T>
LogicalResult visit(T &&stmt) {
mlir::emitError(loc, "unsupported statement: ")
<< slang::ast::toString(stmt.kind);
return mlir::failure();
}
LogicalResult visitInvalid(const slang::ast::Statement &stmt) {
mlir::emitError(loc, "invalid statement: ")
<< slang::ast::toString(stmt.kind);
return mlir::failure();
}
};
} // namespace
LogicalResult
Context::convertStatement(const slang::ast::Statement *statement) {
auto loc = convertLocation(statement->sourceRange.start());
return statement->visit(StmtVisitor(*this, loc));
}
// NOLINTEND(misc-no-recursion)

View File

@ -16,6 +16,25 @@ using namespace ImportVerilog;
// Module Member Conversion
//===----------------------------------------------------------------------===//
static moore::ProcedureKind
convertProcedureKind(slang::ast::ProceduralBlockKind kind) {
switch (kind) {
case slang::ast::ProceduralBlockKind::Always:
return moore::ProcedureKind::Always;
case slang::ast::ProceduralBlockKind::AlwaysComb:
return moore::ProcedureKind::AlwaysComb;
case slang::ast::ProceduralBlockKind::AlwaysLatch:
return moore::ProcedureKind::AlwaysLatch;
case slang::ast::ProceduralBlockKind::AlwaysFF:
return moore::ProcedureKind::AlwaysFF;
case slang::ast::ProceduralBlockKind::Initial:
return moore::ProcedureKind::Initial;
case slang::ast::ProceduralBlockKind::Final:
return moore::ProcedureKind::Final;
}
llvm_unreachable("all procedure kinds handled");
}
namespace {
struct MemberVisitor {
Context &context;
@ -69,6 +88,26 @@ struct MemberVisitor {
return success();
}
// Handle procedures.
LogicalResult visit(const slang::ast::ProceduralBlockSymbol &procNode) {
auto procOp = builder.create<moore::ProcedureOp>(
loc, convertProcedureKind(procNode.procedureKind));
procOp.getBodyRegion().emplaceBlock();
OpBuilder::InsertionGuard guard(builder);
builder.setInsertionPointToEnd(procOp.getBody());
return context.convertStatement(&procNode.getBody());
}
// Ignore statement block symbols. These get generated by Slang for blocks
// with variables and other declarations. For example, having an initial
// procedure with a variable declaration, such as `initial begin int x; end`,
// will create the procedure with a block and variable declaration as
// expected, but will also create a `StatementBlockSymbol` with just the
// variable layout _next to_ the initial procedure.
LogicalResult visit(const slang::ast::StatementBlockSymbol &) {
return success();
}
/// Emit an error for all other members.
template <typename T>
LogicalResult visit(T &&node) {

View File

@ -7,14 +7,7 @@
//===----------------------------------------------------------------------===//
#include "ImportVerilogInternals.h"
#include "slang/ast/ASTVisitor.h"
#include "slang/ast/Symbol.h"
#include "slang/ast/symbols/CompilationUnitSymbols.h"
#include "slang/ast/symbols/InstanceSymbols.h"
#include "slang/ast/symbols/VariableSymbols.h"
#include "slang/ast/types/AllTypes.h"
#include "slang/ast/types/Type.h"
#include "slang/syntax/SyntaxVisitor.h"
#include "slang/syntax/AllSyntax.h"
using namespace circt;
using namespace ImportVerilog;

View File

@ -37,8 +37,39 @@ module Parent;
Child child();
endmodule
// CHECK-LABEL: moore.module @Foo
module Foo;
// CHECK-LABEL: moore.module @Basic
module Basic;
// CHECK: %myVar = moore.variable
var myVar;
// CHECK: moore.procedure initial {
// CHECK: }
initial;
// CHECK: moore.procedure final {
// CHECK: }
final begin end
// CHECK: moore.procedure always {
// CHECK: %x = moore.variable
// CHECK: %y = moore.variable
// CHECK: }
always begin
int x;
begin
int y;
end
end
// CHECK: moore.procedure always_comb {
// CHECK: }
always_comb begin end
// CHECK: moore.procedure always_latch {
// CHECK: }
always_latch begin end
// CHECK: moore.procedure always_ff {
// CHECK: }
always_ff @* begin end
endmodule

View File

@ -57,3 +57,22 @@ module Foo;
// expected-error @below {{variable initializer expressions not supported}}
int a = 0;
endmodule
// -----
module Foo;
initial begin
// expected-error @below {{variable initializer expressions not supported}}
automatic int a = 0;
end
endmodule
// -----
module Foo;
int a;
initial begin
// expected-error @below {{unsupported statement}}
release a;
end
endmodule

View File

@ -6,8 +6,21 @@ moore.module @Foo {
moore.instance "foo" @Foo
// CHECK: %myVar = moore.variable : !moore.bit
%myVar = moore.variable : !moore.bit
// CHECK: [[TMP:%.+]] = moore.variable name "myVar" : !moore.bit
moore.variable name "myVar" : !moore.bit
// CHECK: [[TMP:%.+]] = moore.variable name "myVar" %myVar : !moore.bit
moore.variable name "myVar" %myVar : !moore.bit
// CHECK: moore.procedure initial {
// CHECK: moore.procedure final {
// CHECK: moore.procedure always {
// CHECK: moore.procedure always_comb {
// CHECK: moore.procedure always_latch {
// CHECK: moore.procedure always_ff {
moore.procedure initial {}
moore.procedure final {}
moore.procedure always {}
moore.procedure always_comb {}
moore.procedure always_latch {}
moore.procedure always_ff {}
}
// CHECK-LABEL: moore.module @Bar