diff --git a/include/circt-c/Dialect/RTG.h b/include/circt-c/Dialect/RTG.h index 7e51e9bf3566..a7cce5733131 100644 --- a/include/circt-c/Dialect/RTG.h +++ b/include/circt-c/Dialect/RTG.h @@ -40,6 +40,12 @@ MLIR_CAPI_EXPORTED unsigned rtgSequenceTypeGetNumElements(MlirType type); MLIR_CAPI_EXPORTED MlirType rtgSequenceTypeGetElement(MlirType type, unsigned i); +/// If the type is an RTG randomized sequence. +MLIR_CAPI_EXPORTED bool rtgTypeIsARandomizedSequence(MlirType type); + +/// Creates an RTG randomized sequence type in the context. +MLIR_CAPI_EXPORTED MlirType rtgRandomizedSequenceTypeGet(MlirContext ctxt); + /// If the type is an RTG label. MLIR_CAPI_EXPORTED bool rtgTypeIsALabel(MlirType type); diff --git a/include/circt/Dialect/RTG/IR/RTGOps.td b/include/circt/Dialect/RTG/IR/RTGOps.td index 10f2da50e49c..c9aa3c0e390a 100644 --- a/include/circt/Dialect/RTG/IR/RTGOps.td +++ b/include/circt/Dialect/RTG/IR/RTGOps.td @@ -85,17 +85,42 @@ def SequenceClosureOp : RTGOp<"sequence_closure", [ }]; } -def InvokeSequenceOp : RTGOp<"invoke_sequence", []> { - let summary = "invoke a sequence of instructions"; +def RandomizeSequenceOp : RTGOp<"randomize_sequence", []> { + let summary = "randomize the content of a sequence"; let description = [{ - This operation takes a sequence closure as operand and acts as a placeholder - for that sequence instantiated with the arguments in the closure in place. + This operation takes a fully substituted sequence and randomizes its + content. This means, no operations the returned sequence does not contain + any randomization constructs anymore (such as random selection from sets and + bags, or other 'randomize_sequence' operations). + + It is useful to have this operation separate from 'embed_sequence' such that + the exact same sequence (i.e., with the same random choices taken) can be + embedded at multiple places. + It is also useful to have this separate from sequence substitution because + this operation is sensitive to the context, but the substitution values for + a sequence family might already be available in a parent sequence that is + placed on a different context. Thus, not having it separated would mean that + the substitution values must all be passed down as arguments to the child + sequence instead of a a single fully substituted sequence value. + }]; + + let arguments = (ins FullySubstitutedSequenceType:$sequence); + let results = (outs RandomizedSequenceType:$randomizedSequence); + + let assemblyFormat = "$sequence attr-dict"; +} + +def EmbedSequenceOp : RTGOp<"embed_sequence", []> { + let summary = "embed a sequence of instructions into another sequence"; + let description = [{ + This operation takes a fully randomized sequence and embeds it into another + sequence or test at the position of this operation. In particular, this is not any kind of function call, it doesn't set up a stack frame, etc. It behaves as if the sequence of instructions it refers to were directly inlined relacing this operation. }]; - let arguments = (ins FullySubstitutedSequenceType:$sequence); + let arguments = (ins RandomizedSequenceType:$sequence); let assemblyFormat = "$sequence attr-dict"; } diff --git a/include/circt/Dialect/RTG/IR/RTGTypes.td b/include/circt/Dialect/RTG/IR/RTGTypes.td index 876df99912f9..2c7f3e623e16 100644 --- a/include/circt/Dialect/RTG/IR/RTGTypes.td +++ b/include/circt/Dialect/RTG/IR/RTGTypes.td @@ -32,6 +32,17 @@ def SequenceType : RTGTypeDef<"Sequence"> { let assemblyFormat = "(`<` $elementTypes^ `>`)?"; } +def RandomizedSequenceType : RTGTypeDef<"RandomizedSequence"> { + let summary = "handle to a fully randomized sequence"; + let description = [{ + Sequences can contain operations to randomize their content in various ways. + A sequence of this type is guaranteed to not have any such operations + anymore (transitively). + }]; + + let mnemonic = "randomized_sequence"; +} + def FullySubstitutedSequenceType : DialectType($_self) && " "llvm::cast($_self).getElementTypes().empty()">, diff --git a/include/circt/Dialect/RTG/IR/RTGVisitors.h b/include/circt/Dialect/RTG/IR/RTGVisitors.h index d29377bf752a..abe12e7cbe1e 100644 --- a/include/circt/Dialect/RTG/IR/RTGVisitors.h +++ b/include/circt/Dialect/RTG/IR/RTGVisitors.h @@ -42,7 +42,7 @@ class RTGOpVisitor { // RTG tests TestOp, TargetOp, YieldOp, // Sequences - SequenceOp, SequenceClosureOp, InvokeSequenceOp, + SequenceOp, SequenceClosureOp, RandomizeSequenceOp, EmbedSequenceOp, // Sets SetCreateOp, SetSelectRandomOp, SetDifferenceOp, SetUnionOp, SetSizeOp>([&](auto expr) -> ResultType { @@ -88,7 +88,8 @@ class RTGOpVisitor { HANDLE(SequenceOp, Unhandled); HANDLE(SequenceClosureOp, Unhandled); - HANDLE(InvokeSequenceOp, Unhandled); + HANDLE(RandomizeSequenceOp, Unhandled); + HANDLE(EmbedSequenceOp, Unhandled); HANDLE(SetCreateOp, Unhandled); HANDLE(SetSelectRandomOp, Unhandled); HANDLE(SetDifferenceOp, Unhandled); diff --git a/integration_test/Bindings/Python/dialects/rtg.py b/integration_test/Bindings/Python/dialects/rtg.py index f2f3a763f32f..35827450ce11 100644 --- a/integration_test/Bindings/Python/dialects/rtg.py +++ b/integration_test/Bindings/Python/dialects/rtg.py @@ -89,14 +89,18 @@ setTy = rtg.SetType.get(indexTy) bagTy = rtg.BagType.get(indexTy) ireg = rtgtest.IntegerRegisterType.get() + randomizedSequenceTy = rtg.RandomizedSequenceType.get() seq = rtg.SequenceOp( 'seq', TypeAttr.get( - rtg.SequenceType.get([sequenceTy, labelTy, setTy, bagTy, ireg]))) - Block.create_at_start(seq.bodyRegion, - [sequenceTy, labelTy, setTy, bagTy, ireg]) - - # CHECK: rtg.sequence @seq(%{{.*}}: !rtg.sequence, %{{.*}}: !rtg.label, %{{.*}}: !rtg.set, %{{.*}}: !rtg.bag, %{{.*}}: !rtgtest.ireg) + rtg.SequenceType.get( + [sequenceTy, labelTy, setTy, bagTy, ireg, + randomizedSequenceTy]))) + Block.create_at_start( + seq.bodyRegion, + [sequenceTy, labelTy, setTy, bagTy, ireg, randomizedSequenceTy]) + + # CHECK: rtg.sequence @seq(%{{.*}}: !rtg.sequence, %{{.*}}: !rtg.label, %{{.*}}: !rtg.set, %{{.*}}: !rtg.bag, %{{.*}}: !rtgtest.ireg, %{{.*}}: !rtg.randomized_sequence) print(m) with Context() as ctx, Location.unknown(): diff --git a/lib/Bindings/Python/RTGModule.cpp b/lib/Bindings/Python/RTGModule.cpp index 0def74eecdae..69c9946adde5 100644 --- a/lib/Bindings/Python/RTGModule.cpp +++ b/lib/Bindings/Python/RTGModule.cpp @@ -41,6 +41,14 @@ void circt::python::populateDialectRTGSubmodule(py::module &m) { return rtgSequenceTypeGetElement(self, i); }); + mlir_type_subclass(m, "RandomizedSequenceType", rtgTypeIsARandomizedSequence) + .def_classmethod( + "get", + [](py::object cls, MlirContext ctxt) { + return cls(rtgRandomizedSequenceTypeGet(ctxt)); + }, + py::arg("self"), py::arg("ctxt") = nullptr); + mlir_type_subclass(m, "LabelType", rtgTypeIsALabel) .def_classmethod( "get", diff --git a/lib/CAPI/Dialect/RTG.cpp b/lib/CAPI/Dialect/RTG.cpp index 7e0c8b73aab0..3d6b35ba73ae 100644 --- a/lib/CAPI/Dialect/RTG.cpp +++ b/lib/CAPI/Dialect/RTG.cpp @@ -48,6 +48,17 @@ MlirType rtgSequenceTypeGetElement(MlirType type, unsigned i) { return wrap(cast(unwrap(type)).getElementTypes()[i]); } +// RandomizedSequenceType +//===----------------------------------------------------------------------===// + +bool rtgTypeIsARandomizedSequence(MlirType type) { + return isa(unwrap(type)); +} + +MlirType rtgRandomizedSequenceTypeGet(MlirContext ctxt) { + return wrap(RandomizedSequenceType::get(unwrap(ctxt))); +} + // LabelType //===----------------------------------------------------------------------===// diff --git a/lib/Dialect/RTG/Transforms/ElaborationPass.cpp b/lib/Dialect/RTG/Transforms/ElaborationPass.cpp index f2f6bc8d2956..78af04fb60ef 100644 --- a/lib/Dialect/RTG/Transforms/ElaborationPass.cpp +++ b/lib/Dialect/RTG/Transforms/ElaborationPass.cpp @@ -86,6 +86,7 @@ static uint32_t getUniformlyInRange(std::mt19937 &rng, uint32_t a, uint32_t b) { namespace { struct BagStorage; struct SequenceStorage; +struct RandomizedSequenceStorage; struct SetStorage; /// Represents a unique virtual register. @@ -122,7 +123,8 @@ struct LabelValue { /// The abstract base class for elaborated values. using ElaboratorValue = std::variant; + RandomizedSequenceStorage *, SetStorage *, VirtualRegister, + LabelValue>; // NOLINTNEXTLINE(readability-identifier-naming) llvm::hash_code hash_value(const ElaboratorValue &val) { @@ -285,24 +287,19 @@ struct BagStorage { /// Storage object for an '!rtg.sequence'. struct SequenceStorage { - SequenceStorage(StringRef name, StringAttr familyName, - SmallVector &&args) + SequenceStorage(StringAttr familyName, SmallVector &&args) : hashcode(llvm::hash_combine( - name, familyName, - llvm::hash_combine_range(args.begin(), args.end()))), - name(name), familyName(familyName), args(std::move(args)) {} + familyName, llvm::hash_combine_range(args.begin(), args.end()))), + familyName(familyName), args(std::move(args)) {} bool isEqual(const SequenceStorage *other) const { - return hashcode == other->hashcode && name == other->name && - familyName == other->familyName && args == other->args; + return hashcode == other->hashcode && familyName == other->familyName && + args == other->args; } // The cached hashcode to avoid repeated computations. const unsigned hashcode; - // The name of this fully substituted and elaborated sequence. - const StringRef name; - // The name of the sequence family this sequence is derived from. const StringAttr familyName; @@ -310,6 +307,25 @@ struct SequenceStorage { const SmallVector args; }; +/// Storage object for an '!rtg.randomized_sequence'. +struct RandomizedSequenceStorage { + RandomizedSequenceStorage(StringRef name, SequenceStorage *sequence) + : hashcode(llvm::hash_combine(name, sequence)), name(name), + sequence(sequence) {} + + bool isEqual(const RandomizedSequenceStorage *other) const { + return hashcode == other->hashcode && sequence == other->sequence; + } + + // The cached hashcode to avoid repeated computations. + const unsigned hashcode; + + // The name of this fully substituted and elaborated sequence. + const StringRef name; + + const SequenceStorage *sequence; +}; + /// An 'Internalizer' object internalizes storages and takes ownership of them. /// When the initializer object is destroyed, all owned storages are also /// deallocated and thus must not be accessed anymore. @@ -344,6 +360,8 @@ class Internalizer { return internedBags; else if constexpr (std::is_same_v) return internedSequences; + else if constexpr (std::is_same_v) + return internedRandomizedSequences; else static_assert(!sizeof(StorageTy), "no intern set available for this storage type."); @@ -360,6 +378,9 @@ class Internalizer { DenseSet, StorageKeyInfo> internedBags; DenseSet, StorageKeyInfo> internedSequences; + DenseSet, + StorageKeyInfo> + internedRandomizedSequences; }; } // namespace @@ -391,13 +412,20 @@ static void print(size_t val, llvm::raw_ostream &os) { } static void print(SequenceStorage *val, llvm::raw_ostream &os) { - os << "name << " derived from @" - << val->familyName.getValue() << "("; + os << "familyName.getValue() << "("; llvm::interleaveComma(val->args, os, [&](const ElaboratorValue &val) { os << val; }); os << ") at " << val << ">"; } +static void print(RandomizedSequenceStorage *val, llvm::raw_ostream &os) { + os << "name << " derived from @" + << val->sequence->familyName.getValue() << "("; + llvm::interleaveComma(val->sequence->args, os, + [&](const ElaboratorValue &val) { os << val; }); + os << ") at " << val << ">"; +} + static void print(SetStorage *val, llvm::raw_ostream &os) { os << "set, os, @@ -436,7 +464,7 @@ class Materializer { /// Materialize IR representing the provided `ElaboratorValue` and return the /// `Value` or a null value on failure. Value materialize(ElaboratorValue val, Location loc, - std::queue &elabRequests, + std::queue &elabRequests, function_ref emitError) { auto iter = materializedValues.find(val); if (iter != materializedValues.end()) @@ -455,9 +483,9 @@ class Materializer { /// Otherwise, all operations after the materializer's insertion point are /// deleted until `op` is reached. An error is returned if the operation is /// before the insertion point. - LogicalResult materialize(Operation *op, - DenseMap &state, - std::queue &elabRequests) { + LogicalResult + materialize(Operation *op, DenseMap &state, + std::queue &elabRequests) { if (op->getNumRegions() > 0) return op->emitOpError("ops with nested regions must be elaborated away"); @@ -531,7 +559,7 @@ class Materializer { } Value visit(TypedAttr val, Location loc, - std::queue &elabRequests, + std::queue &elabRequests, function_ref emitError) { // For index attributes (and arithmetic operations on them) we use the // index dialect. @@ -560,7 +588,7 @@ class Materializer { } Value visit(size_t val, Location loc, - std::queue &elabRequests, + std::queue &elabRequests, function_ref emitError) { Value res = builder.create(loc, val); materializedValues[val] = res; @@ -568,7 +596,7 @@ class Materializer { } Value visit(bool val, Location loc, - std::queue &elabRequests, + std::queue &elabRequests, function_ref emitError) { Value res = builder.create(loc, val); materializedValues[val] = res; @@ -576,7 +604,7 @@ class Materializer { } Value visit(SetStorage *val, Location loc, - std::queue &elabRequests, + std::queue &elabRequests, function_ref emitError) { SmallVector elements; elements.reserve(val->set.size()); @@ -594,7 +622,7 @@ class Materializer { } Value visit(BagStorage *val, Location loc, - std::queue &elabRequests, + std::queue &elabRequests, function_ref emitError) { SmallVector values, weights; values.reserve(val->bag.size()); @@ -616,14 +644,24 @@ class Materializer { } Value visit(SequenceStorage *val, Location loc, - std::queue &elabRequests, + std::queue &elabRequests, + function_ref emitError) { + emitError() << "materializing a non-randomized sequence not supported yet"; + return Value(); + } + + Value visit(RandomizedSequenceStorage *val, Location loc, + std::queue &elabRequests, function_ref emitError) { elabRequests.push(val); - return builder.create(loc, val->name, ValueRange()); + Value seq = builder.create( + loc, SequenceType::get(builder.getContext(), {}), val->name, + ValueRange{}); + return builder.create(loc, seq); } Value visit(const VirtualRegister &val, Location loc, - std::queue &elabRequests, + std::queue &elabRequests, function_ref emitError) { auto res = builder.create(loc, val.allowedRegs); materializedValues[val] = res; @@ -631,7 +669,7 @@ class Materializer { } Value visit(const LabelValue &val, Location loc, - std::queue &elabRequests, + std::queue &elabRequests, function_ref emitError) { if (val.id == 0) { auto res = builder.create(loc, val.name, ValueRange()); @@ -680,7 +718,7 @@ struct ElaboratorSharedState { /// 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; + std::queue worklist; }; /// Interprets the IR to perform and lower the represented randomizations. @@ -741,19 +779,27 @@ class Elaborator : public RTGOpVisitor> { } FailureOr visitOp(SequenceClosureOp op) { - SmallVector args; - for (auto arg : op.getArgs()) - args.push_back(state.at(arg)); + SmallVector replacements; + for (auto replacement : op.getArgs()) + replacements.push_back(state.at(replacement)); - auto familyName = op.getSequenceAttr(); - auto name = sharedState.names.newName(familyName.getValue()); state[op.getResult()] = - sharedState.internalizer.internalize(name, familyName, - std::move(args)); + sharedState.internalizer.internalize( + op.getSequenceAttr(), std::move(replacements)); return DeletionKind::Delete; } - FailureOr visitOp(InvokeSequenceOp op) { + FailureOr visitOp(RandomizeSequenceOp op) { + auto *seq = get(op.getSequence()); + + auto name = sharedState.names.newName(seq->familyName.getValue()); + state[op.getResult()] = + sharedState.internalizer.internalize(name, + seq); + return DeletionKind::Delete; + } + + FailureOr visitOp(EmbedSequenceOp op) { return DeletionKind::Keep; } @@ -1213,7 +1259,7 @@ LogicalResult ElaborationPass::elaborateModule(ModuleOp moduleOp, if (table.lookup(curr->name)) continue; - auto familyOp = table.lookup(curr->familyName); + auto familyOp = table.lookup(curr->sequence->familyName); // TODO: don't clone if this is the only remaining reference to this // sequence OpBuilder builder(familyOp); @@ -1229,7 +1275,8 @@ LogicalResult ElaborationPass::elaborateModule(ModuleOp moduleOp, Materializer materializer(OpBuilder::atBlockBegin(seqOp.getBody())); Elaborator elaborator(state, materializer); - if (failed(elaborator.elaborate(familyOp.getBodyRegion(), curr->args))) + if (failed(elaborator.elaborate(familyOp.getBodyRegion(), + curr->sequence->args))) return failure(); materializer.finalize(); @@ -1263,29 +1310,35 @@ LogicalResult ElaborationPass::inlineSequences(TestOp testOp, OpBuilder builder(testOp); for (auto iter = testOp.getBody()->begin(); iter != testOp.getBody()->end();) { - auto invokeOp = dyn_cast(&*iter); - if (!invokeOp) { + auto embedOp = dyn_cast(&*iter); + if (!embedOp) { ++iter; continue; } - auto seqClosureOp = - invokeOp.getSequence().getDefiningOp(); - if (!seqClosureOp) - return invokeOp->emitError( - "sequence operand not directly defined by sequence_closure op"); + auto randSeqOp = embedOp.getSequence().getDefiningOp(); + if (!randSeqOp) + return embedOp->emitError("sequence operand not directly defined by " + "'rtg.randomize_sequence' op"); + auto getSeqOp = randSeqOp.getSequence().getDefiningOp(); + if (!getSeqOp) + return randSeqOp->emitError( + "sequence operand not directly defined by 'rtg.sequence_closure' op"); - auto seqOp = table.lookup(seqClosureOp.getSequenceAttr()); + auto seqOp = table.lookup(getSeqOp.getSequenceAttr()); - builder.setInsertionPointAfter(invokeOp); + builder.setInsertionPointAfter(embedOp); IRMapping mapping; for (auto &op : *seqOp.getBody()) builder.clone(op, mapping); (iter++)->erase(); - if (seqClosureOp->use_empty()) - seqClosureOp->erase(); + if (randSeqOp->use_empty()) + randSeqOp->erase(); + + if (getSeqOp->use_empty()) + getSeqOp->erase(); } return success(); diff --git a/test/CAPI/rtg.c b/test/CAPI/rtg.c index 83bc18c717ab..d5748c6a1617 100644 --- a/test/CAPI/rtg.c +++ b/test/CAPI/rtg.c @@ -35,6 +35,17 @@ static void testSequenceType(MlirContext ctx) { mlirTypeDump(sequenceWithArgsTy); } +static void testRandomizedSequenceType(MlirContext ctx) { + MlirType sequenceTy = rtgRandomizedSequenceTypeGet(ctx); + + // CHECK: is_randomized_sequence + fprintf(stderr, rtgTypeIsARandomizedSequence(sequenceTy) + ? "is_randomized_sequence\n" + : "isnot_randomized_sequence\n"); + // CHECK: !rtg.randomized_sequence + mlirTypeDump(sequenceTy); +} + static void testLabelType(MlirContext ctx) { MlirType labelTy = rtgLabelTypeGet(ctx); @@ -114,6 +125,7 @@ int main(int argc, char **argv) { mlirDialectHandleLoadDialect(mlirGetDialectHandle__rtg__(), ctx); testSequenceType(ctx); + testRandomizedSequenceType(ctx); testLabelType(ctx); testSetType(ctx); testBagType(ctx); diff --git a/test/Dialect/RTG/IR/basic.mlir b/test/Dialect/RTG/IR/basic.mlir index 42776cc47585..e53edd58469b 100644 --- a/test/Dialect/RTG/IR/basic.mlir +++ b/test/Dialect/RTG/IR/basic.mlir @@ -1,5 +1,9 @@ // RUN: circt-opt %s --verify-roundtrip | FileCheck %s +// CHECK-LABEL: rtg.sequence @ranomizedSequenceType +// CHECK-SAME: (%{{.*}}: !rtg.randomized_sequence) +rtg.sequence @ranomizedSequenceType(%seq: !rtg.randomized_sequence) {} + // CHECK-LABEL: rtg.sequence @seq rtg.sequence @seq0() { %arg = arith.constant 1 : index @@ -23,18 +27,22 @@ rtg.sequence @seqAttrsAndTypeElements(%arg0: !rtg.sequence) { - // expected-error @below {{use of value '%arg0' expects different type than prior uses: '!rtg.sequence' vs '!rtg.sequence'}} - rtg.invoke_sequence %arg0 +rtg.sequence @seq0(%arg0: !rtg.sequence) { + // expected-error @below {{use of value '%arg0' expects different type than prior uses: '!rtg.randomized_sequence' vs '!rtg.sequence'}} + rtg.embed_sequence %arg0 } // ----- diff --git a/test/Dialect/RTG/Transform/elaboration.mlir b/test/Dialect/RTG/Transform/elaboration.mlir index 5475b8c55d71..0da1cab488fe 100644 --- a/test/Dialect/RTG/Transform/elaboration.mlir +++ b/test/Dialect/RTG/Transform/elaboration.mlir @@ -116,8 +116,9 @@ rtg.sequence @seq0(%arg0: index) { rtg.sequence @seq1(%arg0: index) { %0 = rtg.sequence_closure @seq0(%arg0 : index) + %1 = rtg.randomize_sequence %0 func.call @dummy2(%arg0) : (index) -> () - rtg.invoke_sequence %0 + rtg.embed_sequence %1 func.call @dummy2(%arg0) : (index) -> () } @@ -129,7 +130,8 @@ rtg.test @nestedSequences : !rtg.dict<> { // CHECK: func.call @dummy2 %0 = index.constant 0 %1 = rtg.sequence_closure @seq1(%0 : index) - rtg.invoke_sequence %1 + %2 = rtg.randomize_sequence %1 + rtg.embed_sequence %2 } rtg.sequence @seq2(%arg0: index) { @@ -145,9 +147,11 @@ rtg.test @sameSequenceDifferentArgs : !rtg.dict<> { %0 = index.constant 0 %1 = index.constant 1 %2 = rtg.sequence_closure @seq2(%0 : index) - %3 = rtg.sequence_closure @seq2(%1 : index) - rtg.invoke_sequence %2 - rtg.invoke_sequence %3 + %3 = rtg.randomize_sequence %2 + %4 = rtg.sequence_closure @seq2(%1 : index) + %5 = rtg.randomize_sequence %4 + rtg.embed_sequence %3 + rtg.embed_sequence %5 } rtg.sequence @seq3(%arg0: !rtg.set) { @@ -166,10 +170,12 @@ rtg.test @sequenceClosureFixesRandomization : !rtg.dict<> { %1 = index.constant 1 %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 + %4 = rtg.randomize_sequence %3 + %5 = rtg.sequence_closure @seq3(%2 : !rtg.set) + %6 = rtg.randomize_sequence %5 + rtg.embed_sequence %4 + rtg.embed_sequence %6 + rtg.embed_sequence %4 } // CHECK-LABLE: @indexOps