From bbef1fa6d9f258e1a4eaba4129699ba0fca493ee Mon Sep 17 00:00:00 2001 From: Martin Erhart Date: Tue, 14 Jan 2025 20:28:46 +0000 Subject: [PATCH] [RTG][Elaboration] Support sequences (#7969) * Simplify materializer to work on only one basic block * Remove unused addWorklist argument and use a class field instead * Because the way invocations are elaborated has changed, we don't need the worklist anymore within sequences or tests (we can just iterate over the operations since it's not a graph region), this is a big performance benefit in large tests/sequences. * Do a BFS over the sequence invocation hierarchy such that callees are only elaborated when all parents are elaborated (because only the `sequence_closure` operation should fix the randomization within a sequence, thus they create a clone before elaborating the referenced sequence) * Inline all sequences and remove them. --- .../RTG/Transforms/ElaborationPass.cpp | 441 +++++++++++++----- test/Dialect/RTG/Transform/elaboration.mlir | 70 +++ 2 files changed, 386 insertions(+), 125 deletions(-) diff --git a/lib/Dialect/RTG/Transforms/ElaborationPass.cpp b/lib/Dialect/RTG/Transforms/ElaborationPass.cpp index 9c7316b451eb..b4b7eba95451 100644 --- a/lib/Dialect/RTG/Transforms/ElaborationPass.cpp +++ b/lib/Dialect/RTG/Transforms/ElaborationPass.cpp @@ -16,11 +16,12 @@ #include "circt/Dialect/RTG/IR/RTGOps.h" #include "circt/Dialect/RTG/IR/RTGVisitors.h" #include "circt/Dialect/RTG/Transforms/RTGPasses.h" +#include "circt/Support/Namespace.h" #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/IR/IRMapping.h" #include "mlir/IR/PatternMatch.h" #include "llvm/Support/Debug.h" -#include +#include #include namespace circt { @@ -84,7 +85,7 @@ namespace { /// The abstract base class for elaborated values. struct ElaboratorValue { public: - enum class ValueKind { Attribute, Set, Bag }; + enum class ValueKind { Attribute, Set, Bag, Sequence }; ElaboratorValue(ValueKind kind) : kind(kind) {} virtual ~ElaboratorValue() {} @@ -196,7 +197,6 @@ class SetValue : public ElaboratorValue { // Compute the hash only once at constructor time. const llvm::hash_code cachedHash; }; -} // namespace /// Holds an evaluated value of a `BagType`'d value. class BagValue : public ElaboratorValue { @@ -252,6 +252,60 @@ class BagValue : public ElaboratorValue { const llvm::hash_code cachedHash; }; +/// Holds an evaluated value of a `SequenceType`'d value. +class SequenceValue : public ElaboratorValue { +public: + SequenceValue(StringRef name, StringAttr familyName, + SmallVector &&args) + : ElaboratorValue(ValueKind::Sequence), name(name), + familyName(familyName), args(std::move(args)), + cachedHash(llvm::hash_combine( + llvm::hash_combine_range(this->args.begin(), this->args.end()), + name, familyName)) {} + + // Implement LLVMs RTTI + static bool classof(const ElaboratorValue *val) { + return val->getKind() == ValueKind::Sequence; + } + + llvm::hash_code getHashValue() const override { return cachedHash; } + + bool isEqual(const ElaboratorValue &other) const override { + auto *otherSeq = dyn_cast(&other); + if (!otherSeq) + return false; + + if (cachedHash != otherSeq->cachedHash) + return false; + + return name == otherSeq->name && familyName == otherSeq->familyName && + args == otherSeq->args; + } + +#ifndef NDEBUG + void print(llvm::raw_ostream &os) const override { + os << "print(os); }); + os << ") at " << this << ">"; + } +#endif + + StringRef getName() const { return name; } + StringAttr getFamilyName() const { return familyName; } + ArrayRef getArgs() const { return args; } + +private: + const StringRef name; + const StringAttr familyName; + const SmallVector args; + + // Compute the hash only once at constructor time. + const llvm::hash_code cachedHash; +}; +} // namespace + #ifndef NDEBUG static llvm::raw_ostream &operator<<(llvm::raw_ostream &os, const ElaboratorValue &value) { @@ -299,27 +353,39 @@ namespace { /// Construct an SSA value from a given elaborated value. class Materializer { public: - Value materialize(ElaboratorValue *val, Block *block, Location loc, + Value materialize(ElaboratorValue *val, Location loc, + std::queue &elabRequests, function_ref emitError) { - auto iter = materializedValues.find({val, block}); + assert(block && "must call reset before calling this function"); + + auto iter = materializedValues.find(val); if (iter != materializedValues.end()) return iter->second; - auto [builderIter, _] = - builderPerBlock.insert({block, OpBuilder::atBlockBegin(block)}); - OpBuilder builder = builderIter->second; + LLVM_DEBUG(llvm::dbgs() << "Materializing " << *val << "\n\n"); + OpBuilder builder(block, insertionPoint); return TypeSwitch(val) - .Case( - [&](auto val) { return visit(val, builder, loc, emitError); }) + .Case([&](auto val) { + return visit(val, builder, loc, elabRequests, emitError); + }) .Default([](auto val) { assert(false && "all cases must be covered above"); return Value(); }); } + Materializer &reset(Block *block) { + materializedValues.clear(); + integerValues.clear(); + this->block = block; + insertionPoint = block->begin(); + return *this; + } + private: Value visit(AttributeValue *val, OpBuilder &builder, Location loc, + std::queue &elabRequests, function_ref emitError) { auto attr = val->getAttr(); @@ -330,7 +396,7 @@ class Materializer { ->getLoadedDialect() ->materializeConstant(builder, attr, attr.getType(), loc) ->getResult(0); - materializedValues[{val, builder.getBlock()}] = res; + materializedValues[val] = res; return res; } @@ -347,16 +413,17 @@ class Materializer { } Value res = op->getResult(0); - materializedValues[{val, builder.getBlock()}] = res; + materializedValues[val] = res; return res; } Value visit(SetValue *val, OpBuilder &builder, Location loc, + std::queue &elabRequests, function_ref emitError) { SmallVector elements; elements.reserve(val->getSet().size()); for (auto *el : val->getSet()) { - auto materialized = materialize(el, builder.getBlock(), loc, emitError); + auto materialized = materialize(el, loc, elabRequests, emitError); if (!materialized) return Value(); @@ -364,29 +431,29 @@ class Materializer { } auto res = builder.create(loc, val->getType(), elements); - materializedValues[{val, builder.getBlock()}] = res; + materializedValues[val] = res; return res; } Value visit(BagValue *val, OpBuilder &builder, Location loc, + std::queue &elabRequests, function_ref emitError) { SmallVector values, weights; values.reserve(val->getBag().size()); weights.reserve(val->getBag().size()); for (auto [val, weight] : val->getBag()) { - auto materializedVal = - materialize(val, builder.getBlock(), loc, emitError); + auto materializedVal = materialize(val, loc, elabRequests, emitError); if (!materializedVal) return Value(); - auto iter = integerValues.find({weight, builder.getBlock()}); + auto iter = integerValues.find(weight); Value materializedWeight; if (iter != integerValues.end()) { materializedWeight = iter->second; } else { materializedWeight = builder.create( loc, builder.getIndexAttr(weight)); - integerValues[{weight, builder.getBlock()}] = materializedWeight; + integerValues[weight] = materializedWeight; } values.push_back(materializedVal); @@ -395,22 +462,30 @@ class Materializer { auto res = builder.create(loc, val->getType(), values, weights); - materializedValues[{val, builder.getBlock()}] = res; + materializedValues[val] = res; return res; } + Value visit(SequenceValue *val, OpBuilder &builder, Location loc, + std::queue &elabRequests, + function_ref emitError) { + elabRequests.push(val); + return builder.create(loc, val->getName(), ValueRange()); + } + private: /// Cache values we have already materialized to reuse them later. We start /// with an insertion point at the start of the block and cache the (updated) /// insertion point such that future materializations can also reuse previous /// materializations without running into dominance issues (or requiring /// additional checks to avoid them). - DenseMap, Value> materializedValues; - DenseMap, Value> integerValues; + DenseMap materializedValues; + DenseMap integerValues; /// Cache the builders to continue insertions at their current insertion point /// for the reason stated above. - DenseMap builderPerBlock; + Block *block; + Block::iterator insertionPoint; }; /// Used to signal to the elaboration driver whether the operation should be @@ -418,15 +493,13 @@ class Materializer { enum class DeletionKind { Keep, Delete }; /// Interprets the IR to perform and lower the represented randomizations. -class Elaborator : public RTGOpVisitor, - function_ref> { +class Elaborator : public RTGOpVisitor> { public: - using RTGBase = RTGOpVisitor, - function_ref>; + using RTGBase = RTGOpVisitor>; using RTGBase::visitOp; using RTGBase::visitRegisterOp; - Elaborator(SymbolTable &table, std::mt19937 &rng) : rng(rng) {} + Elaborator(SymbolTable &table, std::mt19937 &rng) : rng(rng), table(table) {} /// Helper to perform internalization and keep track of interpreted value for /// the given SSA value. @@ -440,41 +513,37 @@ class Elaborator : public RTGOpVisitor, } /// Print a nice error message for operations we don't support yet. - FailureOr - visitUnhandledOp(Operation *op, - function_ref addToWorklist) { + FailureOr visitUnhandledOp(Operation *op) { return op->emitOpError("elaboration not supported"); } - FailureOr - visitExternalOp(Operation *op, - function_ref addToWorklist) { + FailureOr visitExternalOp(Operation *op) { // TODO: we only have this to be able to write tests for this pass without // having to add support for more operations for now, so it should be // removed once it is not necessary anymore for writing tests - if (op->use_empty()) { - for (auto &operand : op->getOpOperands()) { - auto emitError = [&]() { - auto diag = op->emitError(); - diag.attachNote(op->getLoc()) - << "while materializing value for operand#" - << operand.getOperandNumber(); - return diag; - }; - Value val = materializer.materialize( - state.at(operand.get()), op->getBlock(), op->getLoc(), emitError); - if (!val) - return failure(); - operand.set(val); - } + if (op->use_empty()) return DeletionKind::Keep; - } - return visitUnhandledOp(op, addToWorklist); + return visitUnhandledOp(op); } - FailureOr - visitOp(SetCreateOp op, function_ref addToWorklist) { + FailureOr visitOp(SequenceClosureOp op) { + SmallVector args; + for (auto arg : op.getArgs()) + args.push_back(state.at(arg)); + + auto familyName = op.getSequenceAttr(); + auto name = names.newName(familyName.getValue()); + internalizeResult(op.getResult(), name, familyName, + std::move(args)); + return DeletionKind::Delete; + } + + FailureOr visitOp(InvokeSequenceOp op) { + return DeletionKind::Keep; + } + + FailureOr visitOp(SetCreateOp op) { SetVector set; for (auto val : op.getElements()) set.insert(state.at(val)); @@ -484,8 +553,7 @@ class Elaborator : public RTGOpVisitor, return DeletionKind::Delete; } - FailureOr - visitOp(SetSelectRandomOp op, function_ref addToWorklist) { + FailureOr visitOp(SetSelectRandomOp op) { auto *set = cast(state.at(op.getSet())); size_t selected; @@ -501,8 +569,7 @@ class Elaborator : public RTGOpVisitor, return DeletionKind::Delete; } - FailureOr - visitOp(SetDifferenceOp op, function_ref addToWorklist) { + FailureOr visitOp(SetDifferenceOp op) { auto original = cast(state.at(op.getOriginal()))->getSet(); auto diff = cast(state.at(op.getDiff()))->getSet(); @@ -514,8 +581,7 @@ class Elaborator : public RTGOpVisitor, return DeletionKind::Delete; } - FailureOr - visitOp(SetUnionOp op, function_ref addToWorklist) { + FailureOr visitOp(SetUnionOp op) { SetVector result; for (auto set : op.getSets()) result.set_union(cast(state.at(set))->getSet()); @@ -525,16 +591,14 @@ class Elaborator : public RTGOpVisitor, return DeletionKind::Delete; } - FailureOr - visitOp(SetSizeOp op, function_ref addToWorklist) { + FailureOr visitOp(SetSizeOp op) { auto size = cast(state.at(op.getSet()))->getSet().size(); auto sizeAttr = IntegerAttr::get(IndexType::get(op->getContext()), size); internalizeResult(op.getResult(), sizeAttr); return DeletionKind::Delete; } - FailureOr - visitOp(BagCreateOp op, function_ref addToWorklist) { + FailureOr visitOp(BagCreateOp op) { MapVector bag; for (auto [val, multiple] : llvm::zip(op.getElements(), op.getMultiples())) { @@ -551,8 +615,7 @@ class Elaborator : public RTGOpVisitor, return DeletionKind::Delete; } - FailureOr - visitOp(BagSelectRandomOp op, function_ref addToWorklist) { + FailureOr visitOp(BagSelectRandomOp op) { auto *bag = cast(state.at(op.getBag())); SmallVector> prefixSum; @@ -579,8 +642,7 @@ class Elaborator : public RTGOpVisitor, return DeletionKind::Delete; } - FailureOr - visitOp(BagDifferenceOp op, function_ref addToWorklist) { + FailureOr visitOp(BagDifferenceOp op) { auto *original = cast(state.at(op.getOriginal())); auto *diff = cast(state.at(op.getDiff())); @@ -606,8 +668,7 @@ class Elaborator : public RTGOpVisitor, return DeletionKind::Delete; } - FailureOr - visitOp(BagUnionOp op, function_ref addToWorklist) { + FailureOr visitOp(BagUnionOp op) { MapVector result; for (auto bag : op.getBags()) { auto *val = cast(state.at(bag)); @@ -620,17 +681,14 @@ class Elaborator : public RTGOpVisitor, return DeletionKind::Delete; } - FailureOr - visitOp(BagUniqueSizeOp op, function_ref addToWorklist) { + FailureOr visitOp(BagUniqueSizeOp op) { auto size = cast(state.at(op.getBag()))->getBag().size(); auto sizeAttr = IntegerAttr::get(IndexType::get(op->getContext()), size); internalizeResult(op.getResult(), sizeAttr); return DeletionKind::Delete; } - FailureOr - dispatchOpVisitor(Operation *op, - function_ref addToWorklist) { + FailureOr dispatchOpVisitor(Operation *op) { if (op->hasTrait()) { SmallVector result; auto foldResult = op->fold(result); @@ -646,58 +704,121 @@ class Elaborator : public RTGOpVisitor, return DeletionKind::Delete; } - return RTGBase::dispatchOpVisitor(op, addToWorklist); + return RTGBase::dispatchOpVisitor(op); } - LogicalResult elaborate(TestOp testOp) { - LLVM_DEBUG(llvm::dbgs() - << "\n=== Elaborating Test @" << testOp.getSymName() << "\n\n"); + LogicalResult elaborate(SequenceOp family, SequenceOp dest, + ArrayRef args) { + LLVM_DEBUG(llvm::dbgs() << "\n=== Elaborating " << family.getOperationName() + << " @" << family.getSymName() << " into @" + << dest.getSymName() << "\n\n"); - DenseSet visited; - std::deque worklist; - DenseSet toDelete; - for (auto &op : *testOp.getBody()) - if (op.use_empty()) - worklist.push_back(&op); + // Reduce max memory consumption and make sure the values cannot be accessed + // anymore because we deleted the ops above. Clearing should lead to better + // performance than having them as a local here and pass via function + // argument. + state.clear(); + materializer.reset(dest.getBody()); + IRMapping mapping; - while (!worklist.empty()) { - auto *curr = worklist.back(); - if (visited.contains(curr)) { - worklist.pop_back(); - continue; - } + for (auto [arg, elabArg] : + llvm::zip(family.getBody()->getArguments(), args)) + state[arg] = elabArg; - if (curr->getNumRegions() != 0) - return curr->emitOpError("nested regions not supported"); + for (auto &op : *family.getBody()) { + if (op.getNumRegions() != 0) + return op.emitOpError("nested regions not supported"); - bool addedSomething = false; - for (auto val : curr->getOperands()) { - if (state.contains(val)) - continue; + auto result = dispatchOpVisitor(&op); + if (failed(result)) + return failure(); - auto *defOp = val.getDefiningOp(); - assert(defOp && "cannot be a BlockArgument here"); - if (!visited.contains(defOp)) { - worklist.push_back(defOp); - addedSomething = true; + if (*result == DeletionKind::Keep) { + for (auto &operand : op.getOpOperands()) { + if (mapping.contains(operand.get())) + continue; + + auto emitError = [&]() { + auto diag = op.emitError(); + diag.attachNote(op.getLoc()) + << "while materializing value for operand#" + << operand.getOperandNumber(); + return diag; + }; + Value val = materializer.materialize( + state.at(operand.get()), op.getLoc(), worklist, emitError); + if (!val) + return failure(); + + mapping.map(operand.get(), val); } + + OpBuilder builder = OpBuilder::atBlockEnd(dest.getBody()); + builder.clone(op, mapping); } - if (addedSomething) - continue; + LLVM_DEBUG({ + llvm::dbgs() << "Elaborating " << op << " to\n["; + + llvm::interleaveComma(op.getResults(), llvm::dbgs(), [&](auto res) { + if (state.contains(res)) + llvm::dbgs() << *state.at(res); + else + llvm::dbgs() << "unknown"; + }); + + llvm::dbgs() << "]\n\n"; + }); + } + + return success(); + } - auto addToWorklist = [&](Operation *op) { - if (op->use_empty()) - worklist.push_front(op); - }; - auto result = dispatchOpVisitor(curr, addToWorklist); + template + LogicalResult elaborateInPlace(OpTy op) { + LLVM_DEBUG(llvm::dbgs() + << "\n=== Elaborating (in place) " << op.getOperationName() + << " @" << op.getSymName() << "\n\n"); + + // Reduce max memory consumption and make sure the values cannot be accessed + // anymore because we deleted the ops above. Clearing should lead to better + // performance than having them as a local here and pass via function + // argument. + state.clear(); + materializer.reset(op.getBody()); + + SmallVector toDelete; + for (auto &op : *op.getBody()) { + if (op.getNumRegions() != 0) + return op.emitOpError("nested regions not supported"); + + auto result = dispatchOpVisitor(&op); if (failed(result)) return failure(); + if (*result == DeletionKind::Keep) { + for (auto &operand : op.getOpOperands()) { + auto emitError = [&]() { + auto diag = op.emitError(); + diag.attachNote(op.getLoc()) + << "while materializing value for operand#" + << operand.getOperandNumber(); + return diag; + }; + Value val = materializer.materialize( + state.at(operand.get()), op.getLoc(), worklist, emitError); + if (!val) + return failure(); + operand.set(val); + } + } else { // DeletionKind::Delete + toDelete.push_back(&op); + } + LLVM_DEBUG({ - llvm::dbgs() << "Elaborating " << *curr << " to\n["; + llvm::dbgs() << "Elaborating " << op << " to\n["; - llvm::interleaveComma(curr->getResults(), llvm::dbgs(), [&](auto res) { + llvm::interleaveComma(op.getResults(), llvm::dbgs(), [&](auto res) { if (state.contains(res)) llvm::dbgs() << *state.at(res); else @@ -706,31 +827,102 @@ class Elaborator : public RTGOpVisitor, llvm::dbgs() << "]\n\n"; }); + } - if (*result == DeletionKind::Delete) - toDelete.insert(curr); + for (auto *op : llvm::reverse(toDelete)) + op->erase(); - visited.insert(curr); - worklist.pop_back(); + return success(); + } + + LogicalResult inlineSequences(TestOp testOp) { + OpBuilder builder(testOp); + for (auto iter = testOp.getBody()->begin(); + iter != testOp.getBody()->end();) { + auto invokeOp = dyn_cast(&*iter); + if (!invokeOp) { + ++iter; + continue; + } + + auto seqClosureOp = + invokeOp.getSequence().getDefiningOp(); + if (!seqClosureOp) + return invokeOp->emitError( + "sequence operand not directly defined by sequence_closure op"); + + auto seqOp = table.lookup(seqClosureOp.getSequenceAttr()); + + builder.setInsertionPointAfter(invokeOp); + IRMapping mapping; + for (auto &op : *seqOp.getBody()) + builder.clone(op, mapping); + + (iter++)->erase(); + + if (seqClosureOp->use_empty()) + seqClosureOp->erase(); } - // FIXME: this assumes that we didn't query the opaque value from an - // interpreted elaborator value in a way that it can remain used in the IR. - for (auto *op : toDelete) { - op->dropAllUses(); - op->erase(); + return success(); + } + + LogicalResult elaborateModule(ModuleOp moduleOp) { + // Update the name cache + names.clear(); + names.add(moduleOp); + + // Initialize the worklist with the test ops since they cannot be placed by + // other ops. + for (auto testOp : moduleOp.getOps()) + if (failed(elaborateInPlace(testOp))) + return failure(); + + // Do top-down BFS traversal such that elaborating a sequence further down + // does not fix the outcome for multiple placements. + while (!worklist.empty()) { + auto *curr = worklist.front(); + worklist.pop(); + + if (table.lookup(curr->getName())) + continue; + + auto familyOp = table.lookup(curr->getFamilyName()); + // TODO: use 'elaborateInPlace' and don't clone if this is the only + // remaining reference to this sequence + OpBuilder builder(familyOp); + auto seqOp = builder.cloneWithoutRegions(familyOp); + seqOp.getBodyRegion().emplaceBlock(); + seqOp.setSymName(curr->getName()); + table.insert(seqOp); + assert(seqOp.getSymName() == curr->getName() && + "should not have been renamed"); + + if (failed(elaborate(familyOp, seqOp, curr->getArgs()))) + return failure(); } - // Reduce max memory consumption and make sure the values cannot be accessed - // anymore because we deleted the ops above. - state.clear(); - interned.clear(); + // Inline all sequences and remove the operations that place the sequences. + for (auto testOp : moduleOp.getOps()) + if (failed(inlineSequences(testOp))) + return failure(); + + // Remove all sequences since they are not accessible from the outside and + // are not needed anymore since we fully inlined them. + for (auto seqOp : llvm::make_early_inc_range(moduleOp.getOps())) + seqOp->erase(); return success(); } private: std::mt19937 rng; + SymbolTable &table; + Namespace names; + + /// The worklist used to keep track of the test and sequence operations to + /// make sure they are processed top-down (BFS traversal). + std::queue worklist; // A map used to intern elaborator values. We do this such that we can // compare pointers when, e.g., computing set differences, uniquing the @@ -774,9 +966,8 @@ void ElaborationPass::runOnOperation() { std::mt19937 rng(seed); Elaborator elaborator(table, rng); - for (auto testOp : moduleOp.getOps()) - if (failed(elaborator.elaborate(testOp))) - return signalPassFailure(); + if (failed(elaborator.elaborateModule(moduleOp))) + return signalPassFailure(); } void ElaborationPass::cloneTargetsIntoTests(SymbolTable &table) { diff --git a/test/Dialect/RTG/Transform/elaboration.mlir b/test/Dialect/RTG/Transform/elaboration.mlir index 631f58cf47d1..7330c9a1a14b 100644 --- a/test/Dialect/RTG/Transform/elaboration.mlir +++ b/test/Dialect/RTG/Transform/elaboration.mlir @@ -106,6 +106,76 @@ rtg.target @target1 : !rtg.dict { rtg.yield %0 : i32 } +// Unused sequences are removed +// CHECK-NOT: rtg.sequence @unused +rtg.sequence @unused {} + +rtg.sequence @seq0 { +^bb0(%arg0: index): + func.call @dummy5(%arg0) : (index) -> () +} + +rtg.sequence @seq1 { +^bb0(%arg0: index): + %0 = rtg.sequence_closure @seq0(%arg0 : index) + func.call @dummy5(%arg0) : (index) -> () + rtg.invoke_sequence %0 + func.call @dummy5(%arg0) : (index) -> () +} + +// CHECK-LABEL: rtg.test @nestedSequences +rtg.test @nestedSequences : !rtg.dict<> { + // CHECK: arith.constant 0 : index + // CHECK: func.call @dummy5 + // CHECK: func.call @dummy5 + // CHECK: func.call @dummy5 + %0 = arith.constant 0 : index + %1 = rtg.sequence_closure @seq1(%0 : index) + rtg.invoke_sequence %1 +} + +rtg.sequence @seq2 { +^bb0(%arg0: index): + func.call @dummy5(%arg0) : (index) -> () +} + +// CHECK-LABEL: rtg.test @sameSequenceDifferentArgs +rtg.test @sameSequenceDifferentArgs : !rtg.dict<> { + // CHECK: [[C0:%.+]] = arith.constant 0 : index + // CHECK: func.call @dummy5([[C0]]) + // CHECK: [[C1:%.+]] = arith.constant 1 : index + // CHECK: func.call @dummy5([[C1]]) + %0 = arith.constant 0 : index + %1 = arith.constant 1 : index + %2 = rtg.sequence_closure @seq2(%0 : index) + %3 = rtg.sequence_closure @seq2(%1 : index) + rtg.invoke_sequence %2 + rtg.invoke_sequence %3 +} + +rtg.sequence @seq3 { +^bb0(%arg0: !rtg.set): + %0 = rtg.set_select_random %arg0 : !rtg.set // we can't use a custom seed here because it would render the test useless + func.call @dummy5(%0) : (index) -> () +} + +// CHECK-LABEL: rtg.test @sequenceClosureFixesRandomization +rtg.test @sequenceClosureFixesRandomization : !rtg.dict<> { + // CHECK: %c0 = arith.constant 0 : index + // CHECK: func.call @dummy5(%c0 + // CHECK: %c1 = arith.constant 1 : index + // CHECK: func.call @dummy5(%c1 + // CHECK: func.call @dummy5(%c0 + %0 = arith.constant 0 : index + %1 = arith.constant 1 : index + %2 = rtg.set_create %0, %1 : index + %3 = rtg.sequence_closure @seq3(%2 : !rtg.set) + %4 = rtg.sequence_closure @seq3(%2 : !rtg.set) + rtg.invoke_sequence %3 + rtg.invoke_sequence %4 + rtg.invoke_sequence %3 +} + // ----- rtg.test @nestedRegionsNotSupported : !rtg.dict<> {