Skip to content

Commit d676ea7

Browse files
fabianschuikiclaude
andcommitted
[LLHD] Add coroutine, call_coroutine, return ops
Add `llhd.coroutine`, `llhd.call_coroutine`, and `llhd.return` ops to represent suspendable subroutines (SystemVerilog tasks) in the LLHD dialect. Coroutines are `IsolatedFromAbove` function-like ops that can contain `llhd.wait` and `llhd.halt`, just like processes. Calling a coroutine suspends the caller until the coroutine returns. Update the parent constraints on `llhd.wait` and `llhd.halt` to also allow `llhd.coroutine` as a parent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2cfee4e commit d676ea7

File tree

4 files changed

+295
-4
lines changed

4 files changed

+295
-4
lines changed

include/circt/Dialect/LLHD/LLHDOps.td

Lines changed: 175 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ include "circt/Dialect/LLHD/LLHDTypes.td"
1414
include "mlir/IR/EnumAttr.td"
1515
include "mlir/IR/OpAsmInterface.td"
1616
include "mlir/IR/SymbolInterfaces.td"
17+
include "mlir/IR/RegionKindInterface.td"
18+
include "mlir/Interfaces/CallInterfaces.td"
1719
include "mlir/Interfaces/ControlFlowInterfaces.td"
20+
include "mlir/Interfaces/FunctionInterfaces.td"
1821
include "mlir/Interfaces/InferTypeOpInterface.td"
1922
include "mlir/Interfaces/MemorySlotInterfaces.td"
2023
include "mlir/Interfaces/SideEffectInterfaces.td"
@@ -628,10 +631,10 @@ def CombinationalOp : LLHDOp<"combinational", [
628631

629632
def WaitOp : LLHDOp<"wait", [
630633
AttrSizedOperandSegments,
631-
HasParent<"ProcessOp">,
634+
ParentOneOf<["ProcessOp", "CoroutineOp"]>,
632635
Terminator,
633636
]> {
634-
let summary = "Suspend execution of a process";
637+
let summary = "Suspend execution of a process or coroutine";
635638
let description = [{
636639
The `llhd.wait` terminator suspends execution of the parent process until
637640
any of the `observed` values change or a fixed `delay` has passed. Execution
@@ -664,10 +667,10 @@ def WaitOp : LLHDOp<"wait", [
664667
}
665668

666669
def HaltOp : LLHDOp<"halt", [
667-
ParentOneOf<["ProcessOp", "FinalOp"]>,
670+
ParentOneOf<["ProcessOp", "FinalOp", "CoroutineOp"]>,
668671
Terminator,
669672
]> {
670-
let summary = "Terminate execution of a process";
673+
let summary = "Terminate execution of a process or coroutine";
671674
let description = [{
672675
The `llhd.halt` terminator suspends execution of the parent process forever,
673676
effectively terminating it. The `yieldOperands` are yielded as the result
@@ -718,4 +721,172 @@ def YieldOp : LLHDOp<"yield", [
718721
let hasVerifier = 1;
719722
}
720723

724+
//===----------------------------------------------------------------------===//
725+
// Coroutines
726+
//===----------------------------------------------------------------------===//
727+
728+
def CoroutineOp : LLHDOp<"coroutine", [
729+
IsolatedFromAbove,
730+
FunctionOpInterface,
731+
Symbol,
732+
RegionKindInterface,
733+
ProceduralRegion,
734+
RecursiveMemoryEffects,
735+
]> {
736+
let summary = "Define a coroutine";
737+
let description = [{
738+
The `llhd.coroutine` op defines a suspendable subroutine that can be called
739+
from processes or other coroutines. It represents the lowered form of a
740+
SystemVerilog task. Unlike regular functions, coroutines can suspend
741+
execution using `llhd.wait` and terminate using `llhd.halt`, just like
742+
processes. Calling a coroutine via `llhd.call_coroutine` suspends the caller
743+
until the coroutine returns.
744+
745+
Example:
746+
```mlir
747+
llhd.coroutine @waitForClk(%clk: !llhd.ref<i1>) {
748+
%0 = llhd.prb %clk : !llhd.ref<i1>
749+
llhd.wait (%clk : !llhd.ref<i1>), ^resume
750+
^resume:
751+
llhd.return
752+
}
753+
```
754+
}];
755+
756+
let arguments = (ins
757+
SymbolNameAttr:$sym_name,
758+
TypeAttrOf<FunctionType>:$function_type,
759+
OptionalAttr<StrAttr>:$sym_visibility,
760+
OptionalAttr<DictArrayAttr>:$arg_attrs,
761+
OptionalAttr<DictArrayAttr>:$res_attrs
762+
);
763+
let results = (outs);
764+
let regions = (region MinSizedRegion<1>:$body);
765+
766+
let hasCustomAssemblyFormat = 1;
767+
768+
let builders = [
769+
OpBuilder<(ins "mlir::StringAttr":$sym_name,
770+
"mlir::TypeAttr":$function_type), [{
771+
build($_builder, $_state, sym_name, function_type,
772+
/*sym_visibility=*/mlir::StringAttr(),
773+
/*arg_attrs=*/nullptr, /*res_attrs=*/nullptr);
774+
}]>,
775+
OpBuilder<(ins "mlir::StringRef":$sym_name,
776+
"mlir::FunctionType":$function_type), [{
777+
build($_builder, $_state,
778+
$_builder.getStringAttr(sym_name),
779+
mlir::TypeAttr::get(function_type),
780+
/*sym_visibility=*/mlir::StringAttr(),
781+
/*arg_attrs=*/nullptr, /*res_attrs=*/nullptr);
782+
}]>,
783+
];
784+
785+
let extraClassDeclaration = [{
786+
static mlir::RegionKind getRegionKind(unsigned index) {
787+
return mlir::RegionKind::SSACFG;
788+
}
789+
790+
/// Returns the argument types of this coroutine.
791+
mlir::ArrayRef<mlir::Type> getArgumentTypes() {
792+
return getFunctionType().getInputs();
793+
}
794+
795+
/// Returns the result types of this coroutine.
796+
mlir::ArrayRef<mlir::Type> getResultTypes() {
797+
return getFunctionType().getResults();
798+
}
799+
800+
//===------------------------------------------------------------------===//
801+
// CallableOpInterface
802+
//===------------------------------------------------------------------===//
803+
804+
mlir::Region *getCallableRegion() { return &getBody(); }
805+
}];
806+
}
807+
808+
def CallCoroutineOp : LLHDOp<"call_coroutine", [
809+
CallOpInterface,
810+
DeclareOpInterfaceMethods<SymbolUserOpInterface>,
811+
]> {
812+
let summary = "Call a coroutine";
813+
let description = [{
814+
The `llhd.call_coroutine` op calls an `llhd.coroutine`. The calling process
815+
or coroutine suspends until the callee returns. This is only valid inside a
816+
process or another coroutine.
817+
818+
Example:
819+
```mlir
820+
llhd.call_coroutine @waitForClk(%clk) : (!llhd.ref<i1>) -> ()
821+
```
822+
}];
823+
824+
let arguments = (ins FlatSymbolRefAttr:$callee, Variadic<AnyType>:$operands);
825+
let results = (outs Variadic<AnyType>);
826+
827+
let assemblyFormat = [{
828+
$callee `(` $operands `)` attr-dict `:` functional-type(operands, results)
829+
}];
830+
831+
let builders = [
832+
OpBuilder<(ins "CoroutineOp":$coroutine,
833+
CArg<"mlir::ValueRange", "{}">:$operands), [{
834+
build($_builder, $_state, coroutine.getFunctionType().getResults(),
835+
mlir::SymbolRefAttr::get(coroutine), operands);
836+
}]>,
837+
];
838+
839+
let extraClassDeclaration = [{
840+
operand_range getArgOperands() {
841+
return getOperands();
842+
}
843+
MutableOperandRange getArgOperandsMutable() {
844+
return getOperandsMutable();
845+
}
846+
847+
mlir::CallInterfaceCallable getCallableForCallee() {
848+
return (*this)->getAttrOfType<mlir::SymbolRefAttr>("callee");
849+
}
850+
851+
void setCalleeFromCallable(mlir::CallInterfaceCallable callee) {
852+
(*this)->setAttr(getCalleeAttrName(),
853+
llvm::cast<mlir::SymbolRefAttr>(callee));
854+
}
855+
856+
/// CallOpInterface requires ArgAndResultAttrsOpInterface, which needs
857+
/// methods to get/set per-argument and per-result attributes. Call sites
858+
/// don't carry these attributes, so we stub them out as no-ops.
859+
mlir::ArrayAttr getArgAttrsAttr() { return nullptr; }
860+
mlir::ArrayAttr getResAttrsAttr() { return nullptr; }
861+
void setArgAttrsAttr(mlir::ArrayAttr args) {}
862+
void setResAttrsAttr(mlir::ArrayAttr args) {}
863+
mlir::Attribute removeArgAttrsAttr() { return nullptr; }
864+
mlir::Attribute removeResAttrsAttr() { return nullptr; }
865+
}];
866+
}
867+
868+
def ReturnOp : LLHDOp<"return", [
869+
HasParent<"CoroutineOp">,
870+
Terminator,
871+
]> {
872+
let summary = "Return from a coroutine";
873+
let description = [{
874+
The `llhd.return` op terminates execution of the enclosing
875+
`llhd.coroutine` and returns control to the caller. It is the coroutine
876+
equivalent of `llhd.halt` for processes.
877+
878+
Example:
879+
```mlir
880+
llhd.coroutine @myTask() {
881+
llhd.return
882+
}
883+
```
884+
}];
885+
let arguments = (ins Variadic<AnyType>:$operands);
886+
let assemblyFormat = [{
887+
($operands^ `:` type($operands))?
888+
attr-dict
889+
}];
890+
}
891+
721892
#endif // CIRCT_DIALECT_LLHD_LLHDOPS_TD

lib/Dialect/LLHD/IR/LLHDOps.cpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "mlir/IR/Region.h"
2020
#include "mlir/IR/Types.h"
2121
#include "mlir/IR/Value.h"
22+
#include "mlir/Interfaces/FunctionImplementation.h"
2223
#include "llvm/ADT/ArrayRef.h"
2324
#include "llvm/ADT/SmallVector.h"
2425

@@ -919,6 +920,67 @@ GetGlobalSignalOp::verifySymbolUses(SymbolTableCollection &symbolTable) {
919920
return success();
920921
}
921922

923+
//===----------------------------------------------------------------------===//
924+
// CoroutineOp
925+
//===----------------------------------------------------------------------===//
926+
927+
ParseResult CoroutineOp::parse(OpAsmParser &parser, OperationState &result) {
928+
auto buildFuncType =
929+
[](Builder &builder, ArrayRef<Type> argTypes, ArrayRef<Type> results,
930+
function_interface_impl::VariadicFlag,
931+
std::string &) { return builder.getFunctionType(argTypes, results); };
932+
933+
return function_interface_impl::parseFunctionOp(
934+
parser, result, /*allowVariadic=*/false,
935+
getFunctionTypeAttrName(result.name), buildFuncType,
936+
getArgAttrsAttrName(result.name), getResAttrsAttrName(result.name));
937+
}
938+
939+
void CoroutineOp::print(OpAsmPrinter &p) {
940+
function_interface_impl::printFunctionOp(
941+
p, *this, /*isVariadic=*/false, getFunctionTypeAttrName(),
942+
getArgAttrsAttrName(), getResAttrsAttrName());
943+
}
944+
945+
//===----------------------------------------------------------------------===//
946+
// CallCoroutineOp
947+
//===----------------------------------------------------------------------===//
948+
949+
LogicalResult
950+
CallCoroutineOp::verifySymbolUses(SymbolTableCollection &symbolTable) {
951+
auto calleeName = getCalleeAttr();
952+
auto coroutine =
953+
symbolTable.lookupNearestSymbolFrom<CoroutineOp>(*this, calleeName);
954+
if (!coroutine)
955+
return emitOpError() << "'" << calleeName.getValue()
956+
<< "' does not reference a valid 'llhd.coroutine'";
957+
958+
auto type = coroutine.getFunctionType();
959+
if (type.getNumInputs() != getNumOperands())
960+
return emitOpError() << "has " << getNumOperands()
961+
<< " operands, but callee expects "
962+
<< type.getNumInputs();
963+
964+
for (unsigned i = 0, e = type.getNumInputs(); i != e; ++i)
965+
if (getOperand(i).getType() != type.getInput(i))
966+
return emitOpError() << "operand " << i << " type mismatch: expected "
967+
<< type.getInput(i) << ", got "
968+
<< getOperand(i).getType();
969+
970+
if (type.getNumResults() != getNumResults())
971+
return emitOpError() << "has " << getNumResults()
972+
<< " results, but callee returns "
973+
<< type.getNumResults();
974+
975+
for (unsigned i = 0, e = type.getNumResults(); i != e; ++i)
976+
if (getResult(i).getType() != type.getResult(i))
977+
return emitOpError() << "result " << i << " type mismatch: expected "
978+
<< type.getResult(i) << ", got "
979+
<< getResult(i).getType();
980+
981+
return success();
982+
}
983+
922984
//===----------------------------------------------------------------------===//
923985
// Auto-Generated Implementations
924986
//===----------------------------------------------------------------------===//

test/Dialect/LLHD/IR/basic.mlir

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,3 +238,34 @@ llhd.global_signal @GlobalSig2 : i42 init {
238238

239239
// CHECK: llhd.get_global_signal @GlobalSig2 : <i42>
240240
llhd.get_global_signal @GlobalSig2 : <i42>
241+
242+
// CHECK-LABEL: llhd.coroutine @myCoroutine(%arg0: !llhd.ref<i1>)
243+
llhd.coroutine @myCoroutine(%arg0: !llhd.ref<i1>) {
244+
// CHECK-NEXT: llhd.return
245+
llhd.return
246+
}
247+
248+
// CHECK-LABEL: llhd.coroutine private @privateCoroutine()
249+
llhd.coroutine private @privateCoroutine() {
250+
llhd.return
251+
}
252+
253+
// Coroutines can contain wait, halt, and call_coroutine.
254+
// CHECK-LABEL: llhd.coroutine @coroutineWithWaitAndHalt
255+
llhd.coroutine @coroutineWithWaitAndHalt(%arg0: !llhd.ref<i1>, %arg1: i1) {
256+
// CHECK: llhd.call_coroutine @myCoroutine(%arg0) : (!llhd.ref<i1>) -> ()
257+
llhd.call_coroutine @myCoroutine(%arg0) : (!llhd.ref<i1>) -> ()
258+
// CHECK: llhd.wait
259+
llhd.wait (%arg1 : i1), ^bb1
260+
^bb1:
261+
// CHECK: llhd.halt
262+
llhd.halt
263+
}
264+
265+
hw.module @coroutineCallFromProcess(in %clk: !llhd.ref<i1>) {
266+
llhd.process {
267+
// CHECK: llhd.call_coroutine @myCoroutine(%clk) : (!llhd.ref<i1>) -> ()
268+
llhd.call_coroutine @myCoroutine(%clk) : (!llhd.ref<i1>) -> ()
269+
llhd.halt
270+
}
271+
}

test/Dialect/LLHD/IR/errors.mlir

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,30 @@ llhd.global_signal @Foo : i9001
123123
llhd.global_signal @Foo : i42 init {
124124
llvm.unreachable
125125
}
126+
127+
// -----
128+
129+
llhd.coroutine @caller() {
130+
// expected-error @below {{'nonexistent' does not reference a valid 'llhd.coroutine'}}
131+
llhd.call_coroutine @nonexistent() : () -> ()
132+
llhd.return
133+
}
134+
135+
// -----
136+
137+
func.func @notACoroutine() { return }
138+
llhd.coroutine @callerCoroutine() {
139+
// expected-error @below {{'notACoroutine' does not reference a valid 'llhd.coroutine'}}
140+
llhd.call_coroutine @notACoroutine() : () -> ()
141+
llhd.return
142+
}
143+
144+
// -----
145+
146+
// A func.func cannot call an llhd.coroutine via func.call.
147+
llhd.coroutine @someCoroutine() { llhd.return }
148+
func.func @funcCallingCoroutine() {
149+
// expected-error @below {{'someCoroutine' does not reference a valid function}}
150+
func.call @someCoroutine() : () -> ()
151+
return
152+
}

0 commit comments

Comments
 (0)