CIRCT 23.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` whose function
20// type appends an `i64` "now" argument after the captures, and appends an
21// observe bitmask and an `i64` wakeup time result after the process results.
22// The entry block and every block targeted by an `llhd.wait` receive the
23// coroutine's argument types as leading block arguments, and a per-value SSA
24// renaming pass threads each coroutine argument through the CFG so uses see
25// the value bound on the most recent re-entry.
26//
27// - Rewrites each `llhd.wait` into an `arc.coroutine.yield` (or, for a wait
28// that can never resume, an `arc.coroutine.halt`) whose trailing yield
29// operands are an observe bitmask and a wakeup time. The bitmask has one bit
30// per coroutine argument, set for each observed value; the instance re-enters
31// the coroutine when a set argument changes. The wakeup is `now + delay`
32// (with the delay converted to `i64` via `llhd.time_to_int`, leaving the time
33// unit opaque to this pass), or the unit-independent sentinel `-1` when the
34// wait has no delay. Each `llhd.halt` becomes an `arc.coroutine.halt` with an
35// empty mask and the `-1` wakeup.
36//
37// - Replaces the original process with an `arc.coroutine.instance` that
38// reads the current simulation time and 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, in order: indices `0..N-1`
98 /// mirror `captures`, and the final index holds `%now`.
99 SmallVector<Value> entryArgs;
100
101 /// Blocks targeted by an `llhd.wait`. Each receives a copy of the
102 /// coroutine's argument-type prefix as its leading block arguments. (The
103 /// entry block cannot appear here: MLIR entry blocks have no predecessors.)
104 SetVector<Block *> resumeBlocks;
105
106 /// Maps each block argument that stands in for a coroutine argument to its
107 /// argument index `0..N-1`. This covers the entry block's leading arguments
108 /// and every resume block's leading prefix arguments, so multiple block
109 /// arguments map to the same index. Populated by
110 /// `addEntryAndResumeBlockArguments` and used by `rewriteTerminators` to
111 /// build the observe bitmask.
113
114 /// Indicates which arguments are relevant for the sensitivity check. This
115 /// matches the bit-wise OR of all dynamically returned sensitivity masks.
116 SmallVector<bool> staticSensitivityMask;
117};
118
119} // namespace
120
121//===----------------------------------------------------------------------===//
122// Helpers
123//===----------------------------------------------------------------------===//
124
125void ProcessLowering::collectCaptures() {
126 auto &body = processOp.getBody();
127 auto builder = OpBuilder::atBlockBegin(&body.front());
128 SmallDenseSet<Value> captureSet;
129 DenseMap<Operation *, Operation *> clonedConstants;
130 body.walk([&](Operation *op) {
131 for (auto &operand : op->getOpOperands()) {
132 auto value = operand.get();
133
134 // Skip values defined inside the body.
135 if (body.isAncestor(value.getParentRegion()))
136 continue;
137
138 // Remap references to outside-defined constants to clones of the
139 // constants in the entry block.
140 auto result = dyn_cast<OpResult>(value);
141 if (result && result.getOwner()->hasTrait<OpTrait::ConstantLike>()) {
142 auto *&cloned = clonedConstants[result.getOwner()];
143 if (!cloned)
144 cloned = builder.clone(*result.getOwner());
145 operand.set(cloned->getResult(result.getResultNumber()));
146 continue;
147 }
148
149 // Otherwise this is an outside-defined SSA value we need to capture as an
150 // argument of the coroutine.
151 if (captureSet.insert(value).second)
152 captures.push_back(value);
153 }
154 });
155}
156
157void ProcessLowering::createCoroutine() {
158 // Find the parent module and use it as the insertion anchor.
159 auto hwModule = processOp->getParentOfType<hw::HWModuleOp>();
160 assert(hwModule);
161 OpBuilder builder(hwModule);
162
163 // Build the function type:
164 // `(captureTypes..., i64) -> (procResults..., iN mask, i64 wakeup)`, where
165 // the observe bitmask `iN` carries one bit per coroutine argument and the
166 // trailing `i64` is the next wakeup time.
167 auto i64Type = builder.getIntegerType(64);
168 SmallVector<Type> argTypes;
169 for (auto cap : captures)
170 argTypes.push_back(cap.getType());
171 argTypes.push_back(i64Type);
172 auto maskType = builder.getIntegerType(argTypes.size());
173 SmallVector<Type> resultTypes(processOp.getResultTypes());
174 resultTypes.push_back(maskType);
175 resultTypes.push_back(i64Type);
176 auto funcType = builder.getFunctionType(argTypes, resultTypes);
177
178 // Pick a symbol name based on the parent hw.module's name. Insertion into
179 // the symbol table uniquifies the name.
180 auto funcName =
181 builder.getStringAttr(hwModule.getSymName() + ".llhd.process");
182 coroOp = CoroutineDefineOp::create(builder, processOp.getLoc(), funcName,
183 funcType);
184 symbolTable.insert(coroOp);
185
186 // Move the process body wholesale into the coroutine. Block arguments are
187 // added in a separate step.
188 coroOp.getBody().takeBody(processOp.getBody());
189}
190
191/// Prepend the coroutine function-type prefix as leading block arguments to
192/// the entry block and to every block targeted by an `llhd.wait`. After this
193/// step the resume blocks satisfy `arc.coroutine.yield`'s contract (its
194/// destination's first `N` arguments must match the coroutine's function
195/// type) and the entry block's first `N` arguments become the SSA values
196/// that the per-argument SSA renaming step will thread through the CFG.
197///
198/// Predecessor branches of resume blocks that are not `llhd.wait`s (e.g.,
199/// `cf.br` from inside the body looping back to a wait target) have their
200/// successor operands extended with placeholder values — the entry block
201/// arguments themselves — at positions 0..N-1, so that the operand count
202/// keeps matching the resume block's new argument count. The SSA renaming
203/// step run afterwards walks the body and rewrites these references to the
204/// correct reaching definition for the source block.
205///
206/// Uses of the original outside captures inside the body are also rewritten
207/// to the entry block's new capture arguments here, so by the time renaming
208/// runs every reference is to an internal SSA value defined at the entry.
209void ProcessLowering::addEntryAndResumeBlockArguments() {
210 auto &body = coroOp.getBody();
211 auto loc = processOp.getLoc();
212
213 // Assemble the initial resume block argument types that will be provided by
214 // callers entering or re-entering the coroutine.
215 SmallVector<Type> prefixTypes;
216 for (auto capture : captures)
217 prefixTypes.push_back(capture.getType());
218 prefixTypes.push_back(IntegerType::get(coroOp.getContext(), 64));
219
220 // Add arguments to the entry block, recording each one's coroutine argument
221 // index so `rewriteTerminators` can map observed values to bitmask bits.
222 auto &entry = body.front();
223 for (auto [index, type] : llvm::enumerate(prefixTypes)) {
224 entryArgs.push_back(entry.insertArgument(index, type, loc));
225 argIndices[entryArgs.back()] = index;
226 }
227
228 // Collect the resume blocks, which are targets of `llhd.wait` ops.
229 for (auto &block : body)
230 if (auto wait = dyn_cast<llhd::WaitOp>(block.getTerminator()))
231 resumeBlocks.insert(wait.getDest());
232
233 // Pre-add prefix args to each resume block, then extend non-wait predecessor
234 // branches with placeholder operands so the destination operand counts keep
235 // matching.
236 auto prefixSize = prefixTypes.size();
237 for (auto *resumeBlock : resumeBlocks) {
238 for (auto [index, type] : llvm::enumerate(prefixTypes))
239 argIndices[resumeBlock->insertArgument(index, type, loc)] = index;
240
241 // Fix non-wait predecessors. The wait predecessors will become
242 // `arc.coroutine.yield`s in the next step; the yield's
243 // `getSuccessorOperands` treats the prefix as produced (not forwarded) so
244 // its operand count already matches.
245 for (auto *pred : resumeBlock->getPredecessors()) {
246 auto *term = pred->getTerminator();
247 if (isa<llhd::WaitOp>(term))
248 continue;
249 auto branchOp = cast<BranchOpInterface>(term);
250 for (unsigned succIdx = 0, succNum = term->getNumSuccessors();
251 succIdx < succNum; ++succIdx) {
252 if (term->getSuccessor(succIdx) != resumeBlock)
253 continue;
254 auto succOperands = branchOp.getSuccessorOperands(succIdx);
255 SmallVector<Value> newOperands(entryArgs.begin(),
256 entryArgs.begin() + prefixSize);
257 for (auto v : succOperands.getForwardedOperands())
258 newOperands.push_back(v);
259 succOperands.getMutableForwardedOperands().assign(newOperands);
260 }
261 }
262 }
263
264 // Replace in-body uses of the original outside captures with the entry
265 // block's new arguments. The per-argument SSA renaming step then refines
266 // these uses to the right reaching definitions, since each resume block
267 // provides its own set of resume arguments, and multiple resume blocks may
268 // converge on a single block.
269 for (auto [capture, entryArg] : llvm::zip(captures, entryArgs))
270 capture.replaceUsesWithIf(entryArg, [&](OpOperand &use) {
271 return body.isAncestor(use.getOwner()->getParentRegion());
272 });
273}
274
275/// Convert `llhd.wait`/`llhd.halt` terminators to their `arc.coroutine`
276/// equivalents. The wakeup operand is computed against the entry block's
277/// `%now` argument; the per-argument SSA renaming step run afterwards
278/// rewrites that reference to the correct block-local definition. A `llhd.wait`
279/// with an `observed` clause yields an observe bitmask with the bit of each
280/// observed coroutine argument set; ops without a delay and without observed
281/// 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 = entryArgs.back();
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 auto now = llhd::CurrentTimeOp::create(builder, loc);
453 auto nowI64 = llhd::TimeToIntOp::create(builder, loc, now);
454
455 SmallVector<Value> args(captures.begin(), captures.end());
456 args.push_back(nowI64);
457
458 assert(args.size() == staticSensitivityMask.size());
459 auto instanceOp = CoroutineInstanceOp::create(
460 builder, loc, processOp.getResultTypes(),
461 FlatSymbolRefAttr::get(coroOp.getSymNameAttr()), args,
462 builder.getDenseBoolArrayAttr(staticSensitivityMask));
463
464 processOp.replaceAllUsesWith(instanceOp.getResults());
465 processOp.erase();
466}
467
468LogicalResult ProcessLowering::run() {
469 collectCaptures();
470 createCoroutine();
471 addEntryAndResumeBlockArguments();
472 if (failed(rewriteTerminators()))
473 return failure();
474
475 // SSA-rename each coroutine entry argument through the CFG. With a single
476 // block, every use is already in the entry block and refers to the entry
477 // arg directly, so there is nothing to thread; `DominanceInfo` also asserts
478 // on single-block regions.
479 if (!coroOp.getBody().hasOneBlock()) {
480 Liveness liveness(coroOp);
481 DominanceInfo dominance(coroOp);
482 for (unsigned argIdx = 0, argNum = entryArgs.size(); argIdx != argNum;
483 ++argIdx)
484 renameCoroutineArgument(argIdx, liveness, dominance);
485 }
486
487 buildInstance();
488 return success();
489}
490
491//===----------------------------------------------------------------------===//
492// Pass Implementation
493//===----------------------------------------------------------------------===//
494
495namespace {
496struct LowerProcessesPass
497 : public arc::impl::LowerProcessesPassBase<LowerProcessesPass> {
498 void runOnOperation() override;
499};
500} // namespace
501
502void LowerProcessesPass::runOnOperation() {
503 auto module = getOperation();
504 auto &symbolTable = getAnalysis<SymbolTable>();
505
506 bool anyFailed = false;
507 module.walk([&](llhd::ProcessOp op) {
508 ProcessLowering lowering(op, symbolTable);
509 if (failed(lowering.run()))
510 anyFailed = true;
511 });
512 if (anyFailed)
513 signalPassFailure();
514}
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)