CIRCT 24.0.0git
Loading...
Searching...
No Matches
LowerProcesses.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 `llhd.process` ops into `arc.coroutine.define` and
10// `arc.coroutine.instance` pairs so the rest of the arcilator pipeline can
11// schedule them like any other coroutine.
12//
13// For each process, the lowering:
14//
15// - Captures values defined outside the body that are referenced inside.
16// Constant-like ops are cloned into the body; all other values are threaded
17// as coroutine arguments.
18//
19// - Lifts the body into a new top-level `arc.coroutine.define` and appends an
20// observe bitmask and an `i64` wakeup time result after the process results.
21// The entry block and every block targeted by an `llhd.wait` receive the
22// coroutine's argument types as leading block arguments, and a per-value SSA
23// renaming pass threads each coroutine argument through the CFG so uses see
24// the value bound on the most recent re-entry.
25//
26// - Rewrites each `llhd.wait` into an `arc.coroutine.yield` (or, for a wait
27// that can never resume, an `arc.coroutine.halt`) whose trailing yield
28// operands are an observe bitmask and a wakeup time. The bitmask has one bit
29// per coroutine argument, set for each observed value; the instance re-enters
30// the coroutine when a set argument changes. The wakeup is `now + delay`,
31// where `now` is the current simulation time the coroutine reads from the
32// model context via `arc.inferred_context` (with the delay converted to `i64`
33// via `llhd.time_to_int`, leaving the time unit opaque to this pass), or the
34// unit-independent sentinel `-1` when the wait has no delay. Each `llhd.halt`
35// becomes an `arc.coroutine.halt` with an empty mask and the `-1` wakeup.
36//
37// - Replaces the original process with an `arc.coroutine.instance` that
38// forwards the captured values.
39//
40//===----------------------------------------------------------------------===//
41
47#include "mlir/Analysis/Liveness.h"
48#include "mlir/IR/Dominance.h"
49#include "mlir/IR/SymbolTable.h"
50#include "mlir/Interfaces/ControlFlowInterfaces.h"
51#include "mlir/Pass/Pass.h"
52#include "llvm/ADT/DenseMap.h"
53#include "llvm/ADT/SetVector.h"
54#include "llvm/Support/GenericIteratedDominanceFrontier.h"
55
56namespace circt {
57namespace arc {
58#define GEN_PASS_DEF_LOWERPROCESSESPASS
59#include "circt/Dialect/Arc/ArcPasses.h.inc"
60} // namespace arc
61} // namespace circt
62
63using namespace circt;
64using namespace arc;
65using namespace mlir;
67using llvm::SmallDenseSet;
68
69namespace {
70
71/// Worker that lowers a single `llhd.process` op.
72struct ProcessLowering {
73 ProcessLowering(llhd::ProcessOp processOp, SymbolTable &symbolTable)
74 : processOp(processOp), symbolTable(symbolTable) {}
75
76 LogicalResult run();
77
78private:
79 void collectCaptures();
80 void createCoroutine();
81 void addEntryAndResumeBlockArguments();
82 LogicalResult rewriteTerminators();
83 void renameCoroutineArgument(unsigned argIdx, Liveness &liveness,
84 DominanceInfo &dominance);
85 void buildInstance();
86
87 llhd::ProcessOp processOp;
88 SymbolTable &symbolTable;
89
90 /// The coroutine created for this process. Populated by `createCoroutine`.
91 CoroutineDefineOp coroOp;
92
93 /// External SSA values referenced inside the body that need to be threaded
94 /// as coroutine arguments.
95 SmallVector<Value> captures;
96
97 /// Block arguments of the coroutine entry block, mirroring `captures`.
98 SmallVector<Value> entryArgs;
99
100 /// Blocks targeted by an `llhd.wait`. Each receives a copy of the
101 /// coroutine's argument-type prefix as its leading block arguments. (The
102 /// entry block cannot appear here: MLIR entry blocks have no predecessors.)
103 SetVector<Block *> resumeBlocks;
104
105 /// Maps each block argument that stands in for a coroutine argument to its
106 /// argument index `0..N-1`. This covers the entry block's leading arguments
107 /// and every resume block's leading prefix arguments, so multiple block
108 /// arguments map to the same index. Populated by
109 /// `addEntryAndResumeBlockArguments` and used by `rewriteTerminators` to
110 /// build the observe bitmask.
112
113 /// Indicates which arguments are relevant for the sensitivity check. This
114 /// matches the bit-wise OR of all dynamically returned sensitivity masks.
115 SmallVector<bool> staticSensitivityMask;
116
117 /// The inferred `!arc.context` value.
118 Value inferredContext;
119};
120
121} // namespace
122
123//===----------------------------------------------------------------------===//
124// Helpers
125//===----------------------------------------------------------------------===//
126
127void ProcessLowering::collectCaptures() {
128 auto &body = processOp.getBody();
129 auto builder = OpBuilder::atBlockBegin(&body.front());
130 SmallDenseSet<Value> captureSet;
131 DenseMap<Operation *, Operation *> clonedConstants;
132 body.walk([&](Operation *op) {
133 for (auto &operand : op->getOpOperands()) {
134 auto value = operand.get();
135
136 // Skip values defined inside the body.
137 if (body.isAncestor(value.getParentRegion()))
138 continue;
139
140 // Remap references to outside-defined constants to clones of the
141 // constants in the entry block.
142 auto result = dyn_cast<OpResult>(value);
143 if (result && result.getOwner()->hasTrait<OpTrait::ConstantLike>()) {
144 auto *&cloned = clonedConstants[result.getOwner()];
145 if (!cloned)
146 cloned = builder.clone(*result.getOwner());
147 operand.set(cloned->getResult(result.getResultNumber()));
148 continue;
149 }
150
151 // Otherwise this is an outside-defined SSA value we need to capture as an
152 // argument of the coroutine.
153 if (captureSet.insert(value).second)
154 captures.push_back(value);
155 }
156 });
157}
158
159void ProcessLowering::createCoroutine() {
160 // Find the parent module and use it as the insertion anchor.
161 auto hwModule = processOp->getParentOfType<hw::HWModuleOp>();
162 assert(hwModule);
163 OpBuilder builder(hwModule);
164
165 // Build the function type:
166 // `(captureTypes...) -> (procResults..., iN mask, i64 wakeup)`, where
167 // the observe bitmask `iN` carries one bit per coroutine argument and the
168 // trailing `i64` is the next wakeup time.
169 auto i64Type = builder.getIntegerType(64);
170 SmallVector<Type> argTypes;
171 for (auto cap : captures)
172 argTypes.push_back(cap.getType());
173 auto maskType = builder.getIntegerType(argTypes.size());
174 SmallVector<Type> resultTypes(processOp.getResultTypes());
175 resultTypes.push_back(maskType);
176 resultTypes.push_back(i64Type);
177 auto funcType = builder.getFunctionType(argTypes, resultTypes);
178
179 // Pick a symbol name based on the parent hw.module's name. Insertion into
180 // the symbol table uniquifies the name.
181 auto funcName =
182 builder.getStringAttr(hwModule.getSymName() + ".llhd.process");
183 coroOp = CoroutineDefineOp::create(builder, processOp.getLoc(), funcName,
184 funcType);
185 symbolTable.insert(coroOp);
186
187 // Move the process body wholesale into the coroutine. Block arguments are
188 // added in a separate step.
189 coroOp.getBody().takeBody(processOp.getBody());
190
191 // Retrieve the context value in the entry block.
192 builder.setInsertionPointToStart(&coroOp.getBody().front());
193 inferredContext = InferredContextOp::create(builder, coroOp.getLoc());
194}
195
196/// Prepend the coroutine function-type prefix as leading block arguments to
197/// the entry block and to every block targeted by an `llhd.wait`. After this
198/// step the resume blocks satisfy `arc.coroutine.yield`'s contract (its
199/// destination's first `N` arguments must match the coroutine's function
200/// type) and the entry block's first `N` arguments become the SSA values
201/// that the per-argument SSA renaming step will thread through the CFG.
202///
203/// Predecessor branches of resume blocks that are not `llhd.wait`s (e.g.,
204/// `cf.br` from inside the body looping back to a wait target) have their
205/// successor operands extended with placeholder values — the entry block
206/// arguments themselves — at positions 0..N-1, so that the operand count
207/// keeps matching the resume block's new argument count. The SSA renaming
208/// step run afterwards walks the body and rewrites these references to the
209/// correct reaching definition for the source block.
210///
211/// Uses of the original outside captures inside the body are also rewritten
212/// to the entry block's new capture arguments here, so by the time renaming
213/// runs every reference is to an internal SSA value defined at the entry.
214void ProcessLowering::addEntryAndResumeBlockArguments() {
215 auto &body = coroOp.getBody();
216 auto loc = processOp.getLoc();
217
218 // Assemble the initial resume block argument types that will be provided by
219 // callers entering or re-entering the coroutine.
220 auto prefixTypes = TypeRange(captures);
221
222 // Add arguments to the entry block, recording each one's coroutine argument
223 // index so `rewriteTerminators` can map observed values to bitmask bits.
224 auto &entry = body.front();
225 for (auto [index, type] : llvm::enumerate(prefixTypes)) {
226 entryArgs.push_back(entry.insertArgument(index, type, loc));
227 argIndices[entryArgs.back()] = index;
228 }
229
230 // Collect the resume blocks, which are targets of `llhd.wait` ops.
231 for (auto &block : body)
232 if (auto wait = dyn_cast<llhd::WaitOp>(block.getTerminator()))
233 resumeBlocks.insert(wait.getDest());
234
235 // Pre-add prefix args to each resume block, then extend non-wait predecessor
236 // branches with placeholder operands so the destination operand counts keep
237 // matching.
238 auto prefixSize = prefixTypes.size();
239 for (auto *resumeBlock : resumeBlocks) {
240 for (auto [index, type] : llvm::enumerate(prefixTypes))
241 argIndices[resumeBlock->insertArgument(index, type, loc)] = index;
242
243 // Fix non-wait predecessors. The wait predecessors will become
244 // `arc.coroutine.yield`s in the next step; the yield's
245 // `getSuccessorOperands` treats the prefix as produced (not forwarded) so
246 // its operand count already matches.
247 for (auto *pred : resumeBlock->getPredecessors()) {
248 auto *term = pred->getTerminator();
249 if (isa<llhd::WaitOp>(term))
250 continue;
251 auto branchOp = cast<BranchOpInterface>(term);
252 for (unsigned succIdx = 0, succNum = term->getNumSuccessors();
253 succIdx < succNum; ++succIdx) {
254 if (term->getSuccessor(succIdx) != resumeBlock)
255 continue;
256 auto succOperands = branchOp.getSuccessorOperands(succIdx);
257 SmallVector<Value> newOperands(entryArgs.begin(),
258 entryArgs.begin() + prefixSize);
259 for (auto v : succOperands.getForwardedOperands())
260 newOperands.push_back(v);
261 succOperands.getMutableForwardedOperands().assign(newOperands);
262 }
263 }
264 }
265
266 // Replace in-body uses of the original outside captures with the entry
267 // block's new arguments. The per-argument SSA renaming step then refines
268 // these uses to the right reaching definitions, since each resume block
269 // provides its own set of resume arguments, and multiple resume blocks may
270 // converge on a single block.
271 for (auto [capture, entryArg] : llvm::zip(captures, entryArgs))
272 capture.replaceUsesWithIf(entryArg, [&](OpOperand &use) {
273 return body.isAncestor(use.getOwner()->getParentRegion());
274 });
275}
276
277/// Convert `llhd.wait`/`llhd.halt` terminators to their `arc.coroutine`
278/// equivalents. The wakeup operand is the inferred context's current time plus
279/// the wait's delay. A `llhd.wait` with an `observed` clause yields an observe
280/// bitmask with the bit of each observed coroutine argument set; ops without a
281/// delay and without observed values can never resume and are treated as halts.
282LogicalResult ProcessLowering::rewriteTerminators() {
283 // The observe bitmask has one bit per coroutine argument. `argIndices` maps
284 // the block arguments that stand in for coroutine arguments to their bit
285 // index; block arguments beyond that prefix (a wait's forwarded destination
286 // operands) and values defined inside the body are absent from the map and
287 // therefore cannot be observed for changes.
288 auto maskWidth = entryArgs.size();
289 staticSensitivityMask.resize(maskWidth, false);
290 for (auto &block : coroOp.getBody()) {
291 auto *term = block.getTerminator();
292 auto loc = term->getLoc();
293 OpBuilder builder(term);
294
295 // Handle `llhd.wait` ops.
296 if (auto wait = dyn_cast<llhd::WaitOp>(term)) {
297 // Build the observe bitmask from the observed operands. Only operands
298 // that map to a coroutine argument set a bit; internally-defined values
299 // (which can never change) and forwarded block arguments contribute none.
300 APInt maskBits(maskWidth, 0);
301 for (auto observed : wait.getObserved())
302 if (auto it = argIndices.find(observed); it != argIndices.end()) {
303 maskBits.setBit(it->second);
304 staticSensitivityMask[it->second] = true;
305 }
306 auto mask = hw::ConstantOp::create(builder, loc, maskBits);
307
308 // A wait without a delay never resumes on time; use the sentinel `-1` as
309 // its wakeup. It is only a real suspension if it observes something --
310 // otherwise it can never resume and is treated as a halt.
311 if (!wait.getDelay()) {
312 auto never =
313 hw::ConstantOp::create(builder, loc, APInt::getAllOnes(64));
314 SmallVector<Value> yieldOperands(wait.getYieldOperands());
315 yieldOperands.push_back(mask);
316 yieldOperands.push_back(never);
317 if (wait.getObserved().empty())
318 CoroutineHaltOp::create(builder, loc, yieldOperands);
319 else
320 CoroutineYieldOp::create(builder, loc, yieldOperands,
321 wait.getDestOperands(), wait.getDest());
322 wait.erase();
323 continue;
324 }
325
326 auto now = CurrentTimeOp::create(builder, loc, inferredContext);
327 auto delay = llhd::TimeToIntOp::create(builder, loc, wait.getDelay());
328 auto wakeup = comb::AddOp::create(builder, loc, now, delay);
329 SmallVector<Value> yieldOperands(wait.getYieldOperands());
330 yieldOperands.push_back(mask);
331 yieldOperands.push_back(wakeup);
332 CoroutineYieldOp::create(builder, loc, yieldOperands,
333 wait.getDestOperands(), wait.getDest());
334 wait.erase();
335 continue;
336 }
337
338 // Handle `llhd.halt` ops.
339 if (auto halt = dyn_cast<llhd::HaltOp>(term)) {
340 auto mask = hw::ConstantOp::create(builder, loc, APInt(maskWidth, 0));
341 auto never = hw::ConstantOp::create(builder, loc, APInt::getAllOnes(64));
342 SmallVector<Value> yieldOperands(halt.getYieldOperands());
343 yieldOperands.push_back(mask);
344 yieldOperands.push_back(never);
345 CoroutineHaltOp::create(builder, loc, yieldOperands);
346 halt.erase();
347 continue;
348 }
349 }
350
351 return success();
352}
353
354/// SSA-rename uses of the coroutine's `argIdx`-th entry-block argument so
355/// that every use observes the reaching definition for its block.
356///
357/// The entry block's argument and the corresponding pre-added argument on
358/// each resume block are the per-block "redefinitions" of this value. Any
359/// block where two such reaching definitions converge — the iterated
360/// dominance frontier of the redefining blocks, restricted to blocks where
361/// the value is live-in — receives a fresh merge block argument and any
362/// branches reaching it gain the appropriate successor operand. Inside each
363/// block we then rewrite uses to that block's reaching definition.
364///
365/// This mirrors the capture rewriting we do in `LowerCoroutines` as part of the
366/// `captureValue` function.
367void ProcessLowering::renameCoroutineArgument(unsigned argIdx,
368 Liveness &liveness,
369 DominanceInfo &dominance) {
370 auto &body = coroOp.getBody();
371 auto entryArg = entryArgs[argIdx];
372 auto *entryBlock = &body.front();
373
374 // Defining blocks of the value: the entry block plus every resume block.
375 SmallPtrSet<Block *, 8> definingBlocks;
376 definingBlocks.insert(entryBlock);
377 for (auto *resumeBlock : resumeBlocks)
378 definingBlocks.insert(resumeBlock);
379
380 // Restrict the IDF to blocks where the value is live-in.
381 SmallPtrSet<Block *, 16> liveInBlocks;
382 for (auto &block : body)
383 if (liveness.getLiveness(&block)->isLiveIn(entryArg))
384 liveInBlocks.insert(&block);
385
386 auto &domTree = dominance.getDomTree(&body);
387 // Second template arg `false` means forward (not post-) dominance frontier.
388 llvm::IDFCalculatorBase<Block, false> idfCalculator(domTree);
389 idfCalculator.setDefiningBlocks(definingBlocks);
390 idfCalculator.setLiveInBlocks(liveInBlocks);
391 SmallVector<Block *> mergeBlocks;
392 idfCalculator.calculate(mergeBlocks);
393
394 SmallPtrSet<Block *, 16> mergeBlockSet(mergeBlocks.begin(),
395 mergeBlocks.end());
396
397 // Walk the dominator tree from the entry block, threading the reaching
398 // definition through each block.
399 struct WorklistItem {
400 DominanceInfoNode *domNode;
401 Value reachingDef;
402 };
403 SmallVector<WorklistItem> worklist;
404 worklist.push_back({domTree.getNode(entryBlock), entryArg});
405
406 while (!worklist.empty()) {
407 auto item = worklist.pop_back_val();
408 auto *block = item.domNode->getBlock();
409
410 if (resumeBlocks.contains(block)) {
411 item.reachingDef = block->getArgument(argIdx);
412 } else if (mergeBlockSet.contains(block)) {
413 item.reachingDef =
414 block->addArgument(entryArg.getType(), entryArg.getLoc());
415 }
416
417 // Rewrite uses in this block (including nested regions and successor
418 // operands of the terminator) to the reaching definition.
419 if (item.reachingDef != entryArg)
420 block->walk([&](Operation *nested) {
421 nested->replaceUsesOfWith(entryArg, item.reachingDef);
422 });
423
424 // For each edge leaving this block into a merge block, forward the
425 // reaching definition by appending it to the branch op's successor
426 // operands. Resume blocks are skipped even when the IDF also flags them
427 // as merge blocks: their prefix slots are filled by the runtime on the
428 // yield path and by the placeholder operands fixed up by the RAUW above
429 // on the non-yield paths, so an append would push an extra operand past
430 // the block's argument count.
431 auto *terminator = block->getTerminator();
432 if (auto branchOp = dyn_cast<BranchOpInterface>(terminator)) {
433 for (auto &blockOperand : terminator->getBlockOperands()) {
434 auto *succ = blockOperand.get();
435 if (!mergeBlockSet.contains(succ) || resumeBlocks.contains(succ))
436 continue;
437 branchOp.getSuccessorOperands(blockOperand.getOperandNumber())
438 .append(item.reachingDef);
439 }
440 }
441
442 for (auto *child : item.domNode->children())
443 worklist.push_back({child, item.reachingDef});
444 }
445}
446
447/// Create an `arc.coroutine.instance` that instantiates the outlined coroutine
448/// in place of the old `llhd.process`.
449void ProcessLowering::buildInstance() {
450 OpBuilder builder(processOp);
451 auto loc = processOp.getLoc();
452
453 assert(captures.size() == staticSensitivityMask.size());
454 auto instanceOp = CoroutineInstanceOp::create(
455 builder, loc, processOp.getResultTypes(),
456 FlatSymbolRefAttr::get(coroOp.getSymNameAttr()), captures,
457 builder.getDenseBoolArrayAttr(staticSensitivityMask));
458
459 processOp.replaceAllUsesWith(instanceOp.getResults());
460 processOp.erase();
461}
462
463LogicalResult ProcessLowering::run() {
464 collectCaptures();
465 createCoroutine();
466 addEntryAndResumeBlockArguments();
467 if (failed(rewriteTerminators()))
468 return failure();
469
470 // SSA-rename each coroutine entry argument through the CFG. With a single
471 // block, every use is already in the entry block and refers to the entry
472 // arg directly, so there is nothing to thread; `DominanceInfo` also asserts
473 // on single-block regions.
474 if (!coroOp.getBody().hasOneBlock()) {
475 Liveness liveness(coroOp);
476 DominanceInfo dominance(coroOp);
477 for (unsigned argIdx = 0, argNum = entryArgs.size(); argIdx != argNum;
478 ++argIdx)
479 renameCoroutineArgument(argIdx, liveness, dominance);
480 }
481
482 buildInstance();
483 return success();
484}
485
486//===----------------------------------------------------------------------===//
487// Pass Implementation
488//===----------------------------------------------------------------------===//
489
490namespace {
491struct LowerProcessesPass
492 : public arc::impl::LowerProcessesPassBase<LowerProcessesPass> {
493 void runOnOperation() override;
494};
495} // namespace
496
497void LowerProcessesPass::runOnOperation() {
498 auto module = getOperation();
499 auto &symbolTable = getAnalysis<SymbolTable>();
500
501 bool anyFailed = false;
502 module.walk([&](llhd::ProcessOp op) {
503 ProcessLowering lowering(op, symbolTable);
504 if (failed(lowering.run()))
505 anyFailed = true;
506 });
507 if (anyFailed)
508 signalPassFailure();
509}
assert(baseType &&"element must be base type")
create(data_type, value)
Definition hw.py:433
Definition arc.py:1
The InstanceGraph op interface, see InstanceGraphInterface.td for more details.
int run(Type[Generator] generator=CppGenerator, List[str] cmdline_args=sys.argv)