Skip to content

Commit

Permalink
[Moore] Improve WaitEventOp, lower to LLHD
Browse files Browse the repository at this point in the history
Rework the `moore.wait_event` op to be able to accurately model the
semantics of SystemVerilog's `@...` event control statements. The op now
has a body region which is executed to detect a relevant change in one
or more interesting values. A new `moore.detect_event` op serves as the
mechanism to encode whether a posedge, negedge, both, or any change at
all on a value should be detected as an event.

Based on this new `moore.wait_event` op we can properly convert most of
the event control statements in `ImportVerilog` to a corresponding MLIR
op. Delay control like `#1ns` is not handled yet.

In the MooreToCore conversion this new op allows us to properly generate
`llhd.wait` operations at the right place that suspend process execution
until an interesting event has occurred. This now also allows us to
support almost all SystemVerilog processes in the lowering. The only
missing ones are `always_comb` and `always_latch` which require an
implicit `llhd.wait` to be inserted. @maerhart has a version of that
lowering almost ready though.

This commit also adds an `llhd.final` op in order to be able to lower
`final` procedures.

Co-authored-by: Martin Erhart <maerhart@outlook.com>
  • Loading branch information
fabianschuiki and maerhart committed Aug 15, 2024
1 parent 351b62f commit 3c8775a
Show file tree
Hide file tree
Showing 12 changed files with 1,054 additions and 409 deletions.
40 changes: 38 additions & 2 deletions include/circt/Dialect/LLHD/IR/LLHDStructureOps.td
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

def ProcessOp : LLHDOp<"process", [
NoRegionArguments,
RecursiveMemoryEffects,
HasParent<"hw::HWModuleOp">
]> {
let summary = "create a process";
Expand Down Expand Up @@ -51,6 +52,39 @@ def ProcessOp : LLHDOp<"process", [
let assemblyFormat = "attr-dict-with-keyword $body";
}

def FinalOp : LLHDOp<"final", [
NoRegionArguments,
RecursiveMemoryEffects,
HasParent<"hw::HWModuleOp">,
]> {
let summary = "A process that runs at the end of simulation";
let description = [{
An `llhd.final` op encapsulates a region of IR that is to be executed after
the last time step of a simulation has completed. This can be used to
implement various forms of state cleanup and tear-down. Some verifications
ops may also want to check that certain final conditions hold at the end of
a simulation run.

The `llhd.wait` terminator is not allowed in `llhd.final` processes since
there is no later time slot for the execution to resume. Control flow must
eventually end in an `llhd.halt` terminator.

Execution order between multiple `llhd.final` ops is undefined.

Example:
```mlir
hw.module @Foo() {
llhd.final {
func.call @printSimulationStatistics() : () -> ()
llhd.halt
}
}
```
}];
let regions = (region MinSizedRegion<1>: $body);
let assemblyFormat = "attr-dict-with-keyword $body";
}

def ConnectOp : LLHDOp<"con", [
SameTypeOperands,
HasParent<"hw::HWModuleOp">
Expand Down Expand Up @@ -108,12 +142,14 @@ def WaitOp : LLHDOp<"wait", [
}];
}

def HaltOp : LLHDOp<"halt", [Terminator, HasParent<"ProcessOp">]> {
def HaltOp : LLHDOp<"halt", [
Terminator,
ParentOneOf<["ProcessOp", "FinalOp"]>
]> {
let summary = "Terminates execution of a process.";
let description = [{
The `halt` instruction terminates execution of a process. All processes
must halt eventually or consist of an infinite loop.
}];

let assemblyFormat = "attr-dict";
}
119 changes: 82 additions & 37 deletions include/circt/Dialect/Moore/MooreOps.td
Original file line number Diff line number Diff line change
Expand Up @@ -358,58 +358,103 @@ def NonBlockingAssignOp : AssignOpBase<"nonblocking_assign"> {
// Statements
//===----------------------------------------------------------------------===//

def None: I32EnumAttrCase<"None", 0, "none">;
/// Transit from 0 to x/z/1, and from x/z to 1.
// Any change on the input.
def AnyChange: I32EnumAttrCase<"AnyChange", 0, "any">;
// A transition from 0 to X/Z/1, or from X/Z to 1.
def PosEdge: I32EnumAttrCase<"PosEdge", 1, "posedge">;
/// Transit from 1 to x/z/0, and from x/z to 0.
// A transition from 1 to X/Z/0, or from X/Z to 0.
def NegEdge: I32EnumAttrCase<"NegEdge", 2, "negedge">;
/// Include the negedge and posedge.
// The combination of `PosEdge` and `NegEdge`.
def BothEdges: I32EnumAttrCase<"BothEdges", 3, "edge">;

def EdgeAtrr: I32EnumAttr<"Edge", "Edge kind",
[None, PosEdge, NegEdge, BothEdges]>{
def EdgeAttr: I32EnumAttr<"Edge", "Edge kind",
[AnyChange, PosEdge, NegEdge, BothEdges]> {
let cppNamespace = "circt::moore";
}

def EventOp : MooreOp<"wait_event", [
HasParent<"ProcedureOp">
def WaitEventOp : MooreOp<"wait_event", [
RecursiveMemoryEffects,
NoRegionArguments,
SingleBlock,
NoTerminator
]> {
let summary = "Detecting posedge and negedge";
let summary = "Suspend execution until an event occurs";
let description = [{
It is introduced by the symbol `@`, and it allows statement execution to
be delayed until the occurrence of some simulation event occurring in a
procedure executing concurrently with this procedure.

For the implicit event control(`@(*)`), there are two situations that are
not automatically added to event expression:
1. Identifiers that only appear in wait or event expressions.
```
always @(*) begin // equivalent to @(b)
@(n) kid = b; // n is not added to @(*)
end
```
2. Identifiers that only appear as a hierarchical_variable_identifier
in the variable_lvalue of the left-hand side of assignments.
```
always @(*) begin
a = b + c; // a is not added to @(*)
end
```
The `moore.wait_event` op suspends execution of the current process until
its body signals that an event has been the detected. Conceptually, the body
of this op is executed whenever any potentially relevant signal has changed.
If one of the contained `moore.detect_event` ops detect an event, execution
resumes after the `moore.wait_event` operation. If no event is detected, the
current process remains suspended.

Example corresponding to the SystemVerilog `@(posedge x, negedge y iff z)`:
```
moore.wait_event {
%0 = moore.read %x : <i1>
%1 = moore.read %y : <i1>
%2 = moore.read %z : <i1>
moore.detect_event posedge %0 : i1
moore.detect_event negedge %1 if %2 : i1
}
```

Example:
The body may also contain any operations necessary to evaluate the event
conditions. For example, the SV `@(posedge ~x iff i == 42)`:
```
@(a, b, c) // none
@(posedge clk) // positive edge
@(negedge clk) // negative edge
@(edge clk) // both edge
@(*) // implicit event control
moore.wait_event {
%0 = moore.read %x : <i1>
%1 = moore.not %0 : i1
%2 = moore.read %i : <i19>
%3 = moore.constant 42 : i19
%4 = moore.eq %2, %3 : i19
moore.detect_event posedge %0 if %4 : i1
}
```

See IEEE 1800-2017 § 9.4.2 "Event control".
}];
let arguments = (ins EdgeAtrr:$edge, UnpackedType:$input);
let results = (outs);
let regions = (region SizedRegion<1>:$body);
let assemblyFormat = [{ attr-dict-with-keyword $body }];
}

def DetectEventOp : MooreOp<"detect_event", [
HasParent<"WaitEventOp">
]> {
let summary = "Check if an event occured within a `wait_event` op";
let description = [{
The `moore.detect_event` op is used inside the body of a `moore.wait_event`
to check if an interesting value change has occurred on its operand. The
`moore.detect_event` op implicitly stores the previous value of its operand
and compares it against the current value to detect an interesting edge:

- `posedge` checks for a low-to-high transition
- `negedge` checks for a high-to-low transition
- `edge` checks for either a `posedge` or a `negedge`
- `any` checks for any value change (including e.g. X to Z)

The edges are detected as follows:

- `0` to `1 X Z`: `posedge`
- `1` to `0 X Z`: `negedge`
- `X Z` to `1`: `posedge`
- `X Z` to `0`: `negedge`

| From | To 0 | To 1 | To X | To Z |
|-------|---------|---------|---------|---------|
| 0 | - | posedge | posedge | posedge |
| 1 | negedge | - | negedge | negedge |
| X | negedge | posedge | - | - |
| Z | negedge | posedge | - | - |

See IEEE 1800-2017 § 9.4.2 "Event control".
}];
let arguments = (ins
EdgeAttr:$edge,
UnpackedType:$input,
Optional<BitType>:$condition
);
let assemblyFormat = [{
$edge $input attr-dict `:` type($input)
$edge $input (`if` $condition^)? attr-dict `:` type($input)
}];
}

Expand Down
81 changes: 43 additions & 38 deletions lib/Conversion/ImportVerilog/Expressions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,32 +62,6 @@ struct RvalueExprVisitor {
return {};
}

/// Helper function to convert a value to its "truthy" boolean value.
Value convertToBool(Value value) {
if (!value)
return {};
if (auto type = dyn_cast_or_null<moore::IntType>(value.getType()))
if (type.getBitSize() == 1)
return value;
if (auto type = dyn_cast_or_null<moore::UnpackedType>(value.getType()))
return builder.create<moore::BoolCastOp>(loc, value);
mlir::emitError(loc, "expression of type ")
<< value.getType() << " cannot be cast to a boolean";
return {};
}

/// Helper function to convert a value to its "truthy" boolean value and
/// convert it to the given domain.
Value convertToBool(Value value, Domain domain) {
value = convertToBool(value);
if (!value)
return {};
auto type = moore::IntType::get(context.getContext(), 1, domain);
if (value.getType() == type)
return value;
return builder.create<moore::ConversionOp>(loc, type, value);
}

// Handle references to the left-hand side of a parent assignment.
Value visit(const slang::ast::LValueReferenceExpression &expr) {
assert(!context.lvalueStack.empty() && "parent assignments push lvalue");
Expand All @@ -98,8 +72,12 @@ struct RvalueExprVisitor {
// Handle named values, such as references to declared variables.
Value visit(const slang::ast::NamedValueExpression &expr) {
if (auto value = context.valueSymbols.lookup(&expr.symbol)) {
if (isa<moore::RefType>(value.getType()))
value = builder.create<moore::ReadOp>(loc, value);
if (isa<moore::RefType>(value.getType())) {
auto readOp = builder.create<moore::ReadOp>(loc, value);
if (context.rvalueReadCallback)
context.rvalueReadCallback(readOp);
value = readOp.getResult();
}
return value;
}

Expand Down Expand Up @@ -229,7 +207,7 @@ struct RvalueExprVisitor {
return createReduction<moore::ReduceXorOp>(arg, true);

case UnaryOperator::LogicalNot:
arg = convertToBool(arg);
arg = context.convertToBool(arg);
if (!arg)
return {};
return builder.create<moore::NotOp>(loc, arg);
Expand Down Expand Up @@ -358,42 +336,42 @@ struct RvalueExprVisitor {
case BinaryOperator::LogicalAnd: {
// TODO: This should short-circuit. Put the RHS code into a separate
// block.
lhs = convertToBool(lhs, domain);
lhs = context.convertToBool(lhs, domain);
if (!lhs)
return {};
rhs = convertToBool(rhs, domain);
rhs = context.convertToBool(rhs, domain);
if (!rhs)
return {};
return builder.create<moore::AndOp>(loc, lhs, rhs);
}
case BinaryOperator::LogicalOr: {
// TODO: This should short-circuit. Put the RHS code into a separate
// block.
lhs = convertToBool(lhs, domain);
lhs = context.convertToBool(lhs, domain);
if (!lhs)
return {};
rhs = convertToBool(rhs, domain);
rhs = context.convertToBool(rhs, domain);
if (!rhs)
return {};
return builder.create<moore::OrOp>(loc, lhs, rhs);
}
case BinaryOperator::LogicalImplication: {
// `(lhs -> rhs)` equivalent to `(!lhs || rhs)`.
lhs = convertToBool(lhs, domain);
lhs = context.convertToBool(lhs, domain);
if (!lhs)
return {};
rhs = convertToBool(rhs, domain);
rhs = context.convertToBool(rhs, domain);
if (!rhs)
return {};
auto notLHS = builder.create<moore::NotOp>(loc, lhs);
return builder.create<moore::OrOp>(loc, notLHS, rhs);
}
case BinaryOperator::LogicalEquivalence: {
// `(lhs <-> rhs)` equivalent to `(lhs && rhs) || (!lhs && !rhs)`.
lhs = convertToBool(lhs, domain);
lhs = context.convertToBool(lhs, domain);
if (!lhs)
return {};
rhs = convertToBool(rhs, domain);
rhs = context.convertToBool(rhs, domain);
if (!rhs)
return {};
auto notLHS = builder.create<moore::NotOp>(loc, lhs);
Expand Down Expand Up @@ -671,7 +649,8 @@ struct RvalueExprVisitor {
mlir::emitError(loc) << "unsupported conditional expression with pattern";
return {};
}
auto value = convertToBool(context.convertRvalueExpression(*cond.expr));
auto value =
context.convertToBool(context.convertRvalueExpression(*cond.expr));
if (!value)
return {};
auto conditionalOp = builder.create<moore::ConditionalOp>(loc, type, value);
Expand Down Expand Up @@ -1044,3 +1023,29 @@ Value Context::convertLvalueExpression(const slang::ast::Expression &expr) {
return expr.visit(LvalueExprVisitor(*this, loc));
}
// NOLINTEND(misc-no-recursion)

/// Helper function to convert a value to its "truthy" boolean value.
Value Context::convertToBool(Value value) {
if (!value)
return {};
if (auto type = dyn_cast_or_null<moore::IntType>(value.getType()))
if (type.getBitSize() == 1)
return value;
if (auto type = dyn_cast_or_null<moore::UnpackedType>(value.getType()))
return builder.create<moore::BoolCastOp>(value.getLoc(), value);
mlir::emitError(value.getLoc(), "expression of type ")
<< value.getType() << " cannot be cast to a boolean";
return {};
}

/// Helper function to convert a value to its "truthy" boolean value and
/// convert it to the given domain.
Value Context::convertToBool(Value value, Domain domain) {
value = convertToBool(value);
if (!value)
return {};
auto type = moore::IntType::get(getContext(), 1, domain);
if (value.getType() == type)
return value;
return builder.create<moore::ConversionOp>(value.getLoc(), type, value);
}
19 changes: 17 additions & 2 deletions lib/Conversion/ImportVerilog/ImportVerilogInternals.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
namespace circt {
namespace ImportVerilog {

using moore::Domain;

/// Port lowering information.
struct PortLowering {
const slang::ast::PortSymbol &ast;
Expand Down Expand Up @@ -102,8 +104,15 @@ struct Context {
Value convertLvalueExpression(const slang::ast::Expression &expr);

// Convert a slang timing control into an MLIR timing control.
LogicalResult
convertTimingControl(const slang::ast::TimingControl &timingControl);
LogicalResult convertTimingControl(const slang::ast::TimingControl &ctrl,
const slang::ast::Statement &stmt);

/// Helper function to convert a value to its "truthy" boolean value.
Value convertToBool(Value value);

/// Helper function to convert a value to its "truthy" boolean value and
/// convert it to the given domain.
Value convertToBool(Value value, Domain domain);

slang::ast::Compilation &compilation;
mlir::ModuleOp intoModuleOp;
Expand Down Expand Up @@ -152,6 +161,12 @@ struct Context {
/// part of the loop body statements will use this information to branch to
/// the correct block.
SmallVector<LoopFrame> loopStack;

/// A listener called for every variable or net being read. This can be used
/// to collect all variables read as part of an expression or statement, for
/// example to populate the list of observed signals in an implicit event
/// control `@*`.
std::function<void(moore::ReadOp)> rvalueReadCallback;
};

} // namespace ImportVerilog
Expand Down
Loading

0 comments on commit 3c8775a

Please sign in to comment.