CIRCT 24.0.0git
Loading...
Searching...
No Matches
LowerCoroutines.cpp
Go to the documentation of this file.
1//===----------------------------------------------------------------------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8//
9// Lower `arc.coroutine.define` ops into state machine functions. Coroutines
10// are functions that can suspend execution at `arc.coroutine.yield` ops and
11// be resumed at a later point. This pass converts each coroutine definition
12// into a plain `func.func` that dispatches on an explicit integer program
13// counter (PC) argument, and that persists all values live across suspension
14// points in an explicit state argument.
15//
16// The lowering proceeds in two steps. The first step lowers each coroutine
17// definition independently of all others:
18//
19// - Rewrite the body such that every resume block receives the values that
20// are live across its suspension points as trailing block arguments, which
21// makes the state that has to be persisted explicit. Values that do not
22// cross a suspension point are left untouched, and constants are cloned
23// into the resumed blocks instead, since they are cheaper to rematerialize
24// than to persist.
25//
26// - Assign a PC value to each resume block and derive the concrete PC and
27// state types. The PC type is an integer just wide enough to encode the
28// start PC (0), one resume PC per resume block (1 to N), and the return and
29// halt sentinels (the two largest values). The state type is a union with
30// one struct variant per resume block that has values to persist.
31//
32// - Replace the definition with a `func.func` that takes the state and PC as
33// leading arguments, dispatches on the PC to either the original entry
34// block or one of the resume blocks (unpacking the corresponding state
35// variant), and returns the new state, resume PC, and yielded values at
36// each suspension point.
37//
38// At this point, the state and PC types of *other* coroutines may still occur
39// as opaque `!arc.coroutine_state` and `!arc.coroutine_pc` types within the
40// lowered functions, for example on calls to nested coroutines. The second
41// step performs a single global sweep over the module that concretizes all
42// such occurrences:
43//
44// - `arc.coroutine.call` ops become plain `func.call` ops, and the auxiliary
45// coroutine ops become integer constants, comparisons, and poison values.
46//
47// - All remaining occurrences of the opaque types -- in block arguments,
48// results of unrelated ops, function signatures, and nested within
49// aggregate types -- are replaced by the concrete types computed in step
50// one. Since a coroutine's persistent state may contain the state of the
51// coroutines it calls, this replacement is recursive. Cyclic state
52// containment, i.e. recursive coroutines, is detected and rejected.
53//
54//===----------------------------------------------------------------------===//
55
60#include "mlir/Analysis/Liveness.h"
61#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h"
62#include "mlir/Dialect/Func/IR/FuncOps.h"
63#include "mlir/Dialect/UB/IR/UBOps.h"
64#include "mlir/IR/AttrTypeSubElements.h"
65#include "mlir/IR/Dominance.h"
66#include "mlir/Pass/Pass.h"
67#include "llvm/ADT/TypeSwitch.h"
68#include "llvm/Support/GenericIteratedDominanceFrontier.h"
69
70namespace circt {
71namespace arc {
72#define GEN_PASS_DEF_LOWERCOROUTINESPASS
73#include "circt/Dialect/Arc/ArcPasses.h.inc"
74} // namespace arc
75} // namespace circt
76
77using namespace circt;
78using namespace arc;
79using namespace mlir;
80
81//===----------------------------------------------------------------------===//
82// Suspension Value Capture
83//===----------------------------------------------------------------------===//
84//
85// The following functions rewrite the body of a coroutine such that every
86// resume block receives the values that are live across the suspension points
87// it resumes from as trailing block arguments. After the lowering, control
88// re-enters a resume block directly from the dispatch logic, so the
89// definitions of these values no longer dominate their uses. Capturing them as
90// block arguments makes the state that has to be persisted explicit, and the
91// lowering picks the arguments up as the contents of the resume block's state
92// variant.
93//
94// Values that do not cross a suspension point are left untouched and keep
95// using their original definition through dominance. Where a control flow
96// path carrying a captured value rejoins a path carrying the original
97// definition, the join block receives a merging block argument as well.
98// Constants are not captured; they are cloned into the using blocks that are
99// reachable from a resume block instead, since they are cheaper to
100// rematerialize on re-entry than to persist across suspension points.
101
102/// Collect the resume blocks of a coroutine body, i.e. the blocks targeted by
103/// yield ops, in region order to make the lowering deterministic.
104static SmallVector<Block *> collectResumeBlocks(Region &region) {
105 SmallPtrSet<Block *, 8> resumeBlockSet;
106 for (auto &block : region)
107 if (auto yieldOp = dyn_cast<CoroutineYieldOp>(block.getTerminator()))
108 resumeBlockSet.insert(yieldOp.getDest());
109 SmallVector<Block *> resumeBlocks;
110 for (auto &block : region)
111 if (resumeBlockSet.contains(&block))
112 resumeBlocks.push_back(&block);
113 return resumeBlocks;
114}
115
116/// Capture a single value as a trailing block argument of each of the given
117/// resume blocks and rewrite all affected uses of the value.
118static LogicalResult captureValue(Value value, Region &region,
119 ArrayRef<Block *> captureBlocks,
120 Liveness &liveness,
121 DominanceInfo &dominance) {
122 auto *defBlock = value.getParentBlock();
123
124 // Determine the blocks that receive an argument for the value. The resume
125 // blocks capture it directly. In addition, wherever a path carrying the
126 // captured value rejoins a path carrying the original definition, the join
127 // block needs a merging argument. These join blocks are the iterated
128 // dominance frontier of the capture blocks plus the original definition,
129 // pruned to the blocks where the value is live-in.
130 auto &domTree = dominance.getDomTree(&region);
131 llvm::IDFCalculatorBase<Block, false> idfCalculator(domTree);
132
133 SmallPtrSet<Block *, 8> definingBlocks(captureBlocks.begin(),
134 captureBlocks.end());
135 definingBlocks.insert(defBlock);
136 idfCalculator.setDefiningBlocks(definingBlocks);
137
138 SmallPtrSet<Block *, 16> liveInBlocks;
139 for (auto &block : region)
140 if (liveness.getLiveness(&block)->isLiveIn(value))
141 liveInBlocks.insert(&block);
142 idfCalculator.setLiveInBlocks(liveInBlocks);
143
144 SmallVector<Block *> mergeBlocks;
145 idfCalculator.calculate(mergeBlocks);
146
147 SmallPtrSet<Block *, 16> argBlocks(mergeBlocks.begin(), mergeBlocks.end());
148 argBlocks.insert(captureBlocks.begin(), captureBlocks.end());
149
150 // Since the value is an SSA value, its defining block dominates its entire
151 // live range, and with it all argument blocks and their predecessors. Walk
152 // the dominator tree from there, tracking the reaching definition of the
153 // value, which is the original value until an argument block redefines it.
154 // Rewrite all uses to the reaching definition of their block, and pass the
155 // definition at the end of each block into any argument block successors.
156 struct WorklistItem {
157 DominanceInfoNode *domNode;
158 Value reachingDef;
159 };
160 SmallVector<WorklistItem> worklist;
161 worklist.push_back({domTree.getNode(defBlock), value});
162
163 while (!worklist.empty()) {
164 auto item = worklist.pop_back_val();
165 auto *block = item.domNode->getBlock();
166
167 if (argBlocks.contains(block))
168 item.reachingDef = block->addArgument(value.getType(), value.getLoc());
169
170 // Rewrite the uses in this block, including in nested regions.
171 if (item.reachingDef != value)
172 block->walk([&](Operation *nestedOp) {
173 nestedOp->replaceUsesOfWith(value, item.reachingDef);
174 });
175
176 // Append the reaching definition to the successor operands of every edge
177 // into an argument block.
178 auto *terminator = block->getTerminator();
179 auto branchOp = dyn_cast<BranchOpInterface>(terminator);
180 for (auto &blockOperand : terminator->getBlockOperands()) {
181 if (!argBlocks.contains(blockOperand.get()))
182 continue;
183 if (!branchOp)
184 return terminator->emitOpError()
185 << "does not implement `BranchOpInterface`; cannot pass value "
186 "into successor block";
187 branchOp.getSuccessorOperands(blockOperand.getOperandNumber())
188 .append(item.reachingDef);
189 }
190
191 for (auto *child : item.domNode->children())
192 worklist.push_back({child, item.reachingDef});
193 }
194
195 return success();
196}
197
198/// Clone a constant into the using blocks that are reachable from a capturing
199/// resume block, where the original definition no longer dominates its uses
200/// after the lowering. All other uses keep using the original.
201static void rematerializeConstant(Value value, Region &region,
202 ArrayRef<Block *> captureBlocks,
203 Liveness &liveness) {
204 // Collect the blocks reachable from a capturing resume block in which the
205 // value is live-in.
206 DenseSet<Block *> resumedBlocks(captureBlocks.begin(), captureBlocks.end());
207 SmallVector<Block *> worklist(captureBlocks.begin(), captureBlocks.end());
208 while (!worklist.empty())
209 for (auto *successor : worklist.pop_back_val()->getSuccessors())
210 if (liveness.getLiveIn(successor).contains(value) &&
211 resumedBlocks.insert(successor).second)
212 worklist.push_back(successor);
213
214 // Redirect the uses in these blocks to a per-block clone of the constant.
215 auto *defOp = value.getDefiningOp();
216 DenseMap<Block *, Value> clones;
217 for (auto &use : llvm::make_early_inc_range(value.getUses())) {
218 auto *block = region.findAncestorBlockInRegion(*use.getOwner()->getBlock());
219 if (!resumedBlocks.contains(block))
220 continue;
221 auto &clone = clones[block];
222 if (!clone) {
223 OpBuilder builder(block, block->begin());
224 clone = builder.clone(*defOp)->getResult(0);
225 }
226 use.set(clone);
227 }
228 if (defOp->use_empty())
229 defOp->erase();
230}
231
232/// Rewrite the body of a coroutine such that every resume block captures the
233/// values that are live across the suspension points it resumes from as
234/// trailing block arguments.
235static LogicalResult captureValuesAcrossSuspension(CoroutineDefineOp defineOp) {
236 Region &region = defineOp.getBody();
237 if (region.hasOneBlock())
238 return success();
239
240 auto resumeBlocks = collectResumeBlocks(region);
241 if (resumeBlocks.empty())
242 return success();
243
244 // Compute the liveness and dominance of the original body once. The
245 // per-value rewrites below only ever change the liveness of the value
246 // currently being processed, never that of the other collected values, and
247 // they add no blocks or control flow edges, so both analyses remain valid
248 // throughout the loop.
249 Liveness liveness(defineOp);
250 DominanceInfo dominance(defineOp);
251
252 // Collect the candidate values up front, since the rewriting below adds new
253 // block arguments and constants which need no further treatment.
254 SmallVector<Value> values;
255 for (auto &block : region) {
256 llvm::append_range(values, block.getArguments());
257 for (auto &op : block)
258 llvm::append_range(values, op.getResults());
259 }
260
261 for (auto value : values) {
262 // Collect the resume blocks where the value is live-in. Values that are
263 // not live across any suspension point still dominate their uses after
264 // the lowering and need no rewriting at all.
265 SmallVector<Block *> captureBlocks;
266 for (auto *block : resumeBlocks)
267 if (liveness.getLiveIn(block).contains(value))
268 captureBlocks.push_back(block);
269 if (captureBlocks.empty())
270 continue;
271
272 // Rematerialize constants in the resumed blocks instead of capturing
273 // them. The inferred Arc context is quasi constant since the coroutine will
274 // never be re-entered with a different context than before. Capture all
275 // other values as trailing resume block arguments.
276 auto *defOp = value.getDefiningOp();
277 if (defOp && (defOp->hasTrait<OpTrait::ConstantLike>() ||
278 isa<InferredContextOp>(defOp))) {
279 rematerializeConstant(value, region, captureBlocks, liveness);
280 continue;
281 }
282 if (failed(captureValue(value, region, captureBlocks, liveness, dominance)))
283 return failure();
284 }
285
286 return success();
287}
288
289//===----------------------------------------------------------------------===//
290// Coroutine Analysis
291//===----------------------------------------------------------------------===//
292
293namespace {
294/// Per-coroutine lowering info: the resume blocks and the concrete PC and
295/// state types derived from the body after the values live across suspension
296/// points have been captured.
297struct CoroutineLowering {
298 /// The original coroutine definition.
299 CoroutineDefineOp defineOp;
300 /// The blocks targeted by yield ops, in region order. The PC value of a
301 /// resume block is its index in this list plus one.
302 SmallVector<Block *> resumeBlocks;
303 /// The union field index holding the persisted state of each resume block,
304 /// or none if the resume block persists no state.
305 SmallVector<std::optional<unsigned>> variantIndices;
306 /// The concrete PC type.
307 IntegerType pcType;
308 /// The concrete state type. This is a union with one struct variant per
309 /// state-persisting resume block, and may still contain the opaque state
310 /// and PC types of other coroutines.
311 Type stateType;
312
313 /// Returns the sentinel PC value indicating that the coroutine returned.
314 uint64_t getReturnPC() { return getHaltPC() - 1; }
315 /// Returns the sentinel PC value indicating that the coroutine halted.
316 uint64_t getHaltPC() {
317 return APInt::getAllOnes(pcType.getWidth()).getZExtValue();
318 }
319 /// Returns the union variant persisting the state of the resume block with
320 /// the given index, or none if the resume block persists no state.
321 std::optional<hw::UnionType::FieldInfo> getVariant(unsigned resumeIndex) {
322 if (auto fieldIndex = variantIndices[resumeIndex])
323 return cast<hw::UnionType>(stateType).getElements()[*fieldIndex];
324 return std::nullopt;
325 }
326};
327} // namespace
328
329/// Determine the resume blocks of a coroutine and derive its concrete PC and
330/// state types. To be called after the values live across suspension points
331/// have been captured, such that the trailing block arguments of each resume
332/// block are exactly the values that have to be persisted.
333static CoroutineLowering analyzeDefinition(CoroutineDefineOp defineOp) {
334 auto *context = defineOp.getContext();
335 CoroutineLowering lowering;
336 lowering.defineOp = defineOp;
337
338 lowering.resumeBlocks = collectResumeBlocks(defineOp.getBody());
339
340 // The PC type must be wide enough to encode the start PC (0), one resume PC
341 // per resume block (1 to N), and the return and halt sentinels (the two
342 // largest values).
343 unsigned pcWidth = llvm::Log2_64_Ceil(lowering.resumeBlocks.size() + 3);
344 lowering.pcType = IntegerType::get(context, pcWidth);
345
346 // Build the state type as a union with one struct variant per resume block
347 // that has values to persist. The variants are named after the resume PC,
348 // and the struct fields after the resume block's trailing arguments.
349 unsigned numCoroutineArgs = defineOp.getArgumentTypes().size();
350 SmallVector<hw::UnionType::FieldInfo> variants;
351 for (auto [index, block] : llvm::enumerate(lowering.resumeBlocks)) {
352 auto persistedArgs = block->getArguments().drop_front(numCoroutineArgs);
353 if (persistedArgs.empty()) {
354 lowering.variantIndices.push_back(std::nullopt);
355 continue;
356 }
357 SmallVector<hw::StructType::FieldInfo> fields;
358 for (auto [fieldIndex, arg] : llvm::enumerate(persistedArgs))
359 fields.push_back(
360 {StringAttr::get(context, "f" + Twine(fieldIndex)), arg.getType()});
361 lowering.variantIndices.push_back(variants.size());
362 variants.push_back({StringAttr::get(context, "r" + Twine(index + 1)),
363 hw::StructType::get(context, fields), 0});
364 }
365 if (variants.empty())
366 lowering.stateType = hw::StructType::get(context, {});
367 else
368 lowering.stateType = hw::UnionType::get(context, variants);
369
370 return lowering;
371}
372
373//===----------------------------------------------------------------------===//
374// Coroutine Lowering
375//===----------------------------------------------------------------------===//
376
377/// Replace a coroutine definition with a state machine function. The function
378/// takes the persistent state and PC as leading arguments and dispatches on
379/// the PC to either the original entry block or, through a trampoline block
380/// that unpacks the corresponding state variant, to one of the resume blocks.
381/// The coroutine terminators become function returns that produce the new
382/// state, resume PC, and yielded values.
383static void lowerDefinition(CoroutineLowering &lowering) {
384 auto defineOp = lowering.defineOp;
385 auto loc = defineOp.getLoc();
386 auto *context = defineOp.getContext();
387 unsigned pcWidth = lowering.pcType.getWidth();
388
389 // Create the replacement function. The signature wraps the coroutine's
390 // function type with the state and PC as leading arguments and results.
391 SmallVector<Type> inputTypes{lowering.stateType, lowering.pcType};
392 llvm::append_range(inputTypes, defineOp.getArgumentTypes());
393 SmallVector<Type> resultTypes{lowering.stateType, lowering.pcType};
394 llvm::append_range(resultTypes, defineOp.getResultTypes());
395 OpBuilder builder(defineOp);
396 auto funcOp =
397 func::FuncOp::create(builder, loc, defineOp.getSymName(),
398 builder.getFunctionType(inputTypes, resultTypes));
399 funcOp.setPrivate();
400
401 // Move the body over and create the dispatch block in front of it.
402 funcOp.getBody().getBlocks().splice(funcOp.getBody().end(),
403 defineOp.getBody().getBlocks());
404 auto *entryBlock = &funcOp.getBody().front();
405 auto *dispatchBlock =
406 builder.createBlock(&funcOp.getBody(), funcOp.getBody().begin());
407 Value stateArg = dispatchBlock->addArgument(lowering.stateType, loc);
408 Value pcArg = dispatchBlock->addArgument(lowering.pcType, loc);
409 SmallVector<Value> callerArgs;
410 for (auto arg : entryBlock->getArguments())
411 callerArgs.push_back(
412 dispatchBlock->addArgument(arg.getType(), arg.getLoc()));
413
414 // Create a trampoline block for each resume block that unpacks the
415 // persisted values from the corresponding state variant and passes them to
416 // the resume block, alongside the fresh caller-supplied arguments.
417 SmallVector<APInt> caseValues;
418 SmallVector<Block *> caseBlocks;
419 for (auto [index, resumeBlock] : llvm::enumerate(lowering.resumeBlocks)) {
420 auto *trampolineBlock = builder.createBlock(resumeBlock);
421 SmallVector<Value> operands = callerArgs;
422 if (auto variant = lowering.getVariant(index)) {
423 Value variantValue =
424 hw::UnionExtractOp::create(builder, loc, stateArg, variant->name);
425 auto explodeOp = hw::StructExplodeOp::create(builder, loc, variantValue);
426 llvm::append_range(operands, explodeOp.getResults());
427 }
428 cf::BranchOp::create(builder, loc, resumeBlock, operands);
429 caseValues.push_back(APInt(pcWidth, index + 1));
430 caseBlocks.push_back(trampolineBlock);
431 }
432
433 // Dispatch on the PC. The start PC enters the original entry block;
434 // passing a return or halt PC is undefined behavior, so those simply fall
435 // into the default case alongside the start PC.
436 builder.setInsertionPointToEnd(dispatchBlock);
437 if (lowering.resumeBlocks.empty()) {
438 cf::BranchOp::create(builder, loc, entryBlock, callerArgs);
439 } else {
440 SmallVector<ValueRange> caseOperands(caseBlocks.size());
441 cf::SwitchOp::create(builder, loc, pcArg, entryBlock, callerArgs,
442 caseValues, caseBlocks, caseOperands);
443 }
444
445 // Replace the coroutine terminators with function returns producing the new
446 // state, resume PC, and yielded values.
447 DenseMap<Block *, unsigned> resumePCs;
448 for (auto [index, block] : llvm::enumerate(lowering.resumeBlocks))
449 resumePCs[block] = index + 1;
450
451 auto lowerTerminator = [&](Operation *op, Value state, uint64_t pc,
452 ValueRange yieldOperands) {
453 OpBuilder builder(op);
454 if (!state)
455 state = ub::PoisonOp::create(builder, op->getLoc(), lowering.stateType,
456 ub::PoisonAttr::get(context));
457 Value pcValue =
458 hw::ConstantOp::create(builder, op->getLoc(), APInt(pcWidth, pc));
459 SmallVector<Value> operands{state, pcValue};
460 llvm::append_range(operands, yieldOperands);
461 func::ReturnOp::create(builder, op->getLoc(), operands);
462 op->erase();
463 };
464
465 for (auto &block : funcOp.getBody()) {
466 TypeSwitch<Operation *>(block.getTerminator())
467 .Case<CoroutineYieldOp>([&](CoroutineYieldOp op) {
468 // Pack the values to persist into the state variant corresponding
469 // to the destination block. Resume blocks without persisted state
470 // have no variant and return a poison state.
471 unsigned pc = resumePCs.lookup(op.getDest());
472 Value state;
473 if (auto variant = lowering.getVariant(pc - 1)) {
474 OpBuilder builder(op);
475 Value variantValue = hw::StructCreateOp::create(
476 builder, op.getLoc(), variant->type, op.getDestOperands());
477 state = hw::UnionCreateOp::create(builder, op.getLoc(),
478 lowering.stateType, variant->name,
479 variantValue);
480 }
481 lowerTerminator(op, state, pc, op.getYieldOperands());
482 })
483 .Case<CoroutineReturnOp>([&](CoroutineReturnOp op) {
484 lowerTerminator(op, Value{}, lowering.getReturnPC(),
485 op.getYieldOperands());
486 })
487 .Case<CoroutineHaltOp>([&](CoroutineHaltOp op) {
488 lowerTerminator(op, Value{}, lowering.getHaltPC(),
489 op.getYieldOperands());
490 });
491 }
492
493 defineOp.erase();
494}
495
496//===----------------------------------------------------------------------===//
497// Pass Implementation
498//===----------------------------------------------------------------------===//
499
500namespace {
501struct LowerCoroutinesPass
502 : public arc::impl::LowerCoroutinesPassBase<LowerCoroutinesPass> {
503 void runOnOperation() override;
504};
505} // namespace
506
507void LowerCoroutinesPass::runOnOperation() {
508 auto module = getOperation();
509
510 // Collect the coroutine definitions to lower, and reject any leftover
511 // coroutine instances in the same pass over the module. Instances are
512 // expected to be lowered into explicit storage and calls beforehand;
513 // rejecting them here avoids breaking their symbol references.
514 SmallVector<CoroutineDefineOp> defineOps;
515 auto walkResult = module->walk([&](Operation *op) {
516 if (auto instanceOp = dyn_cast<CoroutineInstanceOp>(op)) {
517 instanceOp.emitOpError("must be lowered before LowerCoroutines");
518 return WalkResult::interrupt();
519 }
520 if (auto defineOp = dyn_cast<CoroutineDefineOp>(op))
521 defineOps.push_back(defineOp);
522 return WalkResult::advance();
523 });
524 if (walkResult.wasInterrupted())
525 return signalPassFailure();
526
527 // Step 1: Lower each coroutine definition independently. Capture the values
528 // live across suspension points as trailing resume block arguments and
529 // derive the concrete PC and state types. The state types may still contain
530 // opaque types of other coroutines, which the global sweep below
531 // concretizes.
532 DenseMap<StringAttr, CoroutineLowering> lowerings;
533 for (auto defineOp : defineOps) {
534 if (failed(captureValuesAcrossSuspension(defineOp)))
535 return signalPassFailure();
536 lowerings.insert({defineOp.getSymNameAttr(), analyzeDefinition(defineOp)});
537 }
538
539 // Reject recursive coroutines. A coroutine whose persistent state
540 // transitively contains its own state would require unbounded storage.
541 // Detect cycles in the state containment graph with a depth-first
542 // traversal. This must run before step 2, whose recursive type replacement
543 // would not terminate on cycles, and before the definitions are erased, so
544 // the error can point at the offending op.
545 enum class Color { Unvisited, InProgress, Done };
546 DenseMap<StringAttr, Color> colors;
547 std::function<LogicalResult(StringAttr)> checkCycles =
548 [&](StringAttr name) -> LogicalResult {
549 auto it = lowerings.find(name);
550 if (it == lowerings.end())
551 return success();
552 if (colors.lookup(name) == Color::Done)
553 return success();
554 if (colors.lookup(name) == Color::InProgress)
555 return it->second.defineOp.emitOpError(
556 "recursive coroutines are not supported");
557 colors[name] = Color::InProgress;
558 auto result = success();
559 it->second.stateType.walk([&](CoroutineStateType type) {
560 if (failed(checkCycles(type.getCoroutine().getAttr())))
561 result = failure();
562 });
563 colors[name] = Color::Done;
564 return result;
565 };
566 for (auto defineOp : defineOps)
567 if (failed(checkCycles(defineOp.getSymNameAttr())))
568 return signalPassFailure();
569
570 // Replace each coroutine definition with a state machine function,
571 // completing step 1.
572 for (auto defineOp : defineOps)
573 lowerDefinition(lowerings.find(defineOp.getSymNameAttr())->second);
574
575 // Step 2: Concretize all occurrences of the opaque coroutine state and PC
576 // types throughout the module. The replacer recurses into the replacement
577 // types, such that a coroutine state containing the state of a nested
578 // coroutine gets fully concretized; the cycle check above guarantees that
579 // this recursion terminates.
580 bool hasUnknownCoroutines = false;
581 auto lookupLowering =
582 [&](FlatSymbolRefAttr coroutine) -> CoroutineLowering * {
583 auto it = lowerings.find(coroutine.getAttr());
584 if (it != lowerings.end())
585 return &it->second;
586 hasUnknownCoroutines = true;
587 mlir::emitError(module.getLoc())
588 << "coroutine type references unknown coroutine " << coroutine;
589 return nullptr;
590 };
591 AttrTypeReplacer replacer;
592 replacer.addReplacement([&](CoroutineStateType type) -> std::optional<Type> {
593 if (auto *lowering = lookupLowering(type.getCoroutine()))
594 return lowering->stateType;
595 return std::nullopt;
596 });
597 replacer.addReplacement([&](CoroutinePCType type) -> std::optional<Type> {
598 if (auto *lowering = lookupLowering(type.getCoroutine()))
599 return lowering->pcType;
600 return std::nullopt;
601 });
602
603 // Rewrite the ops that consume or produce values of the opaque types. This
604 // must happen before the type sweep below, since some of these ops identify
605 // their coroutine solely through the symbol carried in their types.
606 SmallVector<Operation *> opsToLower;
607 module->walk([&](Operation *op) {
608 if (isa<CoroutineCallOp, CoroutineStartPCOp, CoroutineUndefinedStateOp,
609 CoroutinePCIsReturnOp, CoroutinePCIsHaltOp>(op))
610 opsToLower.push_back(op);
611 });
612
613 // Determine the PC width for a sentinel check. The PC operand either still
614 // has the opaque PC type, or has already been concretized to an integer if
615 // its producer was rewritten earlier in the loop below.
616 auto getPCWidth = [&](Value pc) -> std::optional<unsigned> {
617 if (auto intType = dyn_cast<IntegerType>(pc.getType()))
618 return intType.getWidth();
619 if (auto intType = dyn_cast<IntegerType>(replacer.replace(pc.getType())))
620 return intType.getWidth();
621 return std::nullopt;
622 };
623
624 for (auto *op : opsToLower) {
625 OpBuilder builder(op);
626 TypeSwitch<Operation *>(op)
627 .Case<CoroutineCallOp>([&](CoroutineCallOp op) {
628 // The lowered function signature matches the coroutine call ABI
629 // exactly, so the call maps to a plain function call.
630 auto resultTypes =
631 llvm::map_to_vector(op.getResultTypes(), [&](Type type) {
632 return replacer.replace(type);
633 });
634 auto callOp =
635 func::CallOp::create(builder, op.getLoc(), op.getCalleeAttr(),
636 resultTypes, op.getOperands());
637 op->replaceAllUsesWith(callOp);
638 op->erase();
639 })
640 .Case<CoroutineStartPCOp>([&](CoroutineStartPCOp op) {
641 auto pcType = dyn_cast<IntegerType>(replacer.replace(op.getType()));
642 if (!pcType)
643 return;
644 Value value = hw::ConstantOp::create(builder, op.getLoc(),
645 APInt(pcType.getWidth(), 0));
646 op->replaceAllUsesWith(ValueRange{value});
647 op->erase();
648 })
649 .Case<CoroutineUndefinedStateOp>([&](CoroutineUndefinedStateOp op) {
650 // The state passed on the very first entry into a coroutine is
651 // never read, so any value will do.
652 auto stateType = replacer.replace(op.getType());
653 if (stateType == op.getType())
654 return;
655 Value value =
656 ub::PoisonOp::create(builder, op.getLoc(), stateType,
657 ub::PoisonAttr::get(builder.getContext()));
658 op->replaceAllUsesWith(ValueRange{value});
659 op->erase();
660 })
661 .Case<CoroutinePCIsReturnOp, CoroutinePCIsHaltOp>([&](auto op) {
662 // Use the raw operand instead of the typed ODS getter; the PC may
663 // already have been concretized to an integer (see `getPCWidth`).
664 Value pc = op->getOperand(0);
665 auto pcWidth = getPCWidth(pc);
666 if (!pcWidth)
667 return;
668 auto sentinel = APInt::getAllOnes(*pcWidth);
669 if (isa<CoroutinePCIsReturnOp>(op))
670 sentinel -= 1;
671 Value constValue =
672 hw::ConstantOp::create(builder, op.getLoc(), sentinel);
673 Value cmpValue = comb::ICmpOp::create(
674 builder, op.getLoc(), comb::ICmpPredicate::eq, pc, constValue);
675 op->replaceAllUsesWith(ValueRange{cmpValue});
676 op->erase();
677 });
678 }
679
680 // Sweep over the entire module and replace all remaining occurrences of the
681 // opaque types. This covers block arguments, results of unrelated ops,
682 // function signatures, and types nested within aggregates.
683 replacer.recursivelyReplaceElementsIn(module, /*replaceAttrs=*/true,
684 /*replaceLocs=*/false,
685 /*replaceTypes=*/true);
686 if (hasUnknownCoroutines)
687 return signalPassFailure();
688}
static std::unique_ptr< Context > context
static CoroutineLowering analyzeDefinition(CoroutineDefineOp defineOp)
Determine the resume blocks of a coroutine and derive its concrete PC and state types.
static void lowerDefinition(CoroutineLowering &lowering)
Replace a coroutine definition with a state machine function.
static LogicalResult captureValue(Value value, Region &region, ArrayRef< Block * > captureBlocks, Liveness &liveness, DominanceInfo &dominance)
Capture a single value as a trailing block argument of each of the given resume blocks and rewrite al...
static SmallVector< Block * > collectResumeBlocks(Region &region)
Collect the resume blocks of a coroutine body, i.e.
static LogicalResult captureValuesAcrossSuspension(CoroutineDefineOp defineOp)
Rewrite the body of a coroutine such that every resume block captures the values that are live across...
static void rematerializeConstant(Value value, Region &region, ArrayRef< Block * > captureBlocks, Liveness &liveness)
Clone a constant into the using blocks that are reachable from a capturing resume block,...
static StringAttr append(StringAttr base, const Twine &suffix)
Return a attribute with the specified suffix appended.
create(data_type, value)
Definition hw.py:433
create(elements, Type result_type=None)
Definition hw.py:544
Definition arc.py:1
The InstanceGraph op interface, see InstanceGraphInterface.td for more details.