CIRCT 23.0.0git
Loading...
Searching...
No Matches
ElaborationPass.cpp
Go to the documentation of this file.
1//===- ElaborationPass.cpp - RTG ElaborationPass implementation -----------===//
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// This pass elaborates the random parts of the RTG dialect.
10// It performs randomization top-down, i.e., random constructs in a sequence
11// that is invoked multiple times can yield different randomization results
12// for each invokation.
13//
14//===----------------------------------------------------------------------===//
15
21#include "mlir/Dialect/Arith/IR/Arith.h"
22#include "mlir/Dialect/Index/IR/IndexDialect.h"
23#include "mlir/Dialect/Index/IR/IndexOps.h"
24#include "mlir/Dialect/SCF/IR/SCF.h"
25#include "mlir/IR/IRMapping.h"
26#include "mlir/IR/PatternMatch.h"
27#include "llvm/ADT/DenseMapInfoVariant.h"
28#include "llvm/ADT/ScopeExit.h"
29#include "llvm/Support/Debug.h"
30#include <memory>
31#include <random>
32
33namespace circt {
34namespace rtg {
35#define GEN_PASS_DEF_ELABORATIONPASS
36#include "circt/Dialect/RTG/Transforms/RTGPasses.h.inc"
37} // namespace rtg
38} // namespace circt
39
40using namespace mlir;
41using namespace circt;
42using namespace circt::rtg;
43using llvm::MapVector;
44
45#define DEBUG_TYPE "rtg-elaboration"
46
47namespace {
48/// Helper class to generate random numbers supporting scopes of randomization.
49struct RngScope {
50 RngScope() = delete;
51 RngScope(const RngScope &) = delete;
52 RngScope &operator=(const RngScope &) = delete;
53
54 RngScope(RngScope &&) = default;
55 RngScope &operator=(RngScope &&) = default;
56
57 explicit RngScope(uint32_t seed) : rng(seed) {}
58
59 //===--------------------------------------------------------------------===//
60 // Uniform Distribution Helper
61 //
62 // Simplified version of
63 // https://github.com/llvm/llvm-project/blob/main/libcxx/include/__random/uniform_int_distribution.h
64 // We use our custom version here to get the same results when compiled with
65 // different compiler versions and standard libraries.
66 //===--------------------------------------------------------------------===//
67
68 /// Get a number uniformly at random in the in specified range.
69 uint32_t getUniformlyInRange(uint32_t a, uint32_t b) {
70 const uint32_t diff = b - a + 1;
71 if (diff == 1)
72 return a;
73
74 if (diff == 0)
75 return rng();
76
77 uint32_t mask =
78 std::numeric_limits<uint32_t>::max() >> (32 - llvm::Log2_32_Ceil(diff));
79 uint32_t u;
80 do {
81 u = rng() & mask;
82 } while (u >= diff);
83
84 return u + a;
85 }
86
87 RngScope getNested() { return RngScope(rng()); }
88
89private:
90 std::mt19937 rng;
91};
92} // namespace
93
94//===----------------------------------------------------------------------===//
95// Elaborator Value
96//===----------------------------------------------------------------------===//
97
98namespace {
99struct ArrayStorage;
100struct BagStorage;
101struct SequenceStorage;
102struct RandomizedSequenceStorage;
103struct InterleavedSequenceStorage;
104struct SetStorage;
105struct VirtualRegisterStorage;
106struct UniqueLabelStorage;
107struct TupleStorage;
108struct MemoryStorage;
109struct MemoryBlockStorage;
110struct SymbolicComputationWithIdentityStorage;
111struct SymbolicComputationWithIdentityValue;
112struct SymbolicComputationStorage;
113struct OpaqueExternalStorage;
114struct ContinuationStorage;
115
116/// The abstract base class for elaborated values.
117using ElaboratorValue =
118 std::variant<TypedAttr, BagStorage *, bool, size_t, SequenceStorage *,
119 RandomizedSequenceStorage *, InterleavedSequenceStorage *,
120 SetStorage *, VirtualRegisterStorage *, UniqueLabelStorage *,
121 ArrayStorage *, TupleStorage *, MemoryStorage *,
122 MemoryBlockStorage *, SymbolicComputationWithIdentityStorage *,
123 SymbolicComputationWithIdentityValue *,
124 SymbolicComputationStorage *, OpaqueExternalStorage *,
125 ContinuationStorage *>;
126
127// NOLINTNEXTLINE(readability-identifier-naming)
128llvm::hash_code hash_value(const ElaboratorValue &val) {
129 return std::visit(
130 [&val](const auto &alternative) {
131 // Include index in hash to make sure same value as different
132 // alternatives don't collide.
133 return llvm::hash_combine(val.index(), alternative);
134 },
135 val);
136}
137
138} // namespace
139
140namespace llvm {
141
142template <>
143struct DenseMapInfo<bool> {
144 static unsigned getHashValue(const bool &val) { return val * 37U; }
145
146 static bool isEqual(const bool &lhs, const bool &rhs) { return lhs == rhs; }
147};
148
149} // namespace llvm
150
151//===----------------------------------------------------------------------===//
152// Elaborator Value Storages and Internalization
153//===----------------------------------------------------------------------===//
154
155namespace {
156
157/// Lightweight object to be used as the key for internalization sets. It caches
158/// the hashcode of the internalized object and a pointer to it. This allows a
159/// delayed allocation and construction of the actual object and thus only has
160/// to happen if the object is not already in the set.
161template <typename StorageTy>
162struct HashedStorage {
163 HashedStorage(unsigned hashcode = 0, StorageTy *storage = nullptr)
164 : hashcode(hashcode), storage(storage) {}
165
166 unsigned hashcode;
167 StorageTy *storage;
168};
169
170/// A DenseMapInfo implementation to support 'insert_as' for the internalization
171/// sets. When comparing two 'HashedStorage's we can just compare the already
172/// internalized storage pointers, otherwise we have to call the costly
173/// 'isEqual' method.
174template <typename StorageTy>
175struct StorageKeyInfo {
176 static inline unsigned getHashValue(const HashedStorage<StorageTy> &key) {
177 return key.hashcode;
178 }
179 static inline unsigned getHashValue(const StorageTy &key) {
180 return key.hashcode;
181 }
182
183 static inline bool isEqual(const HashedStorage<StorageTy> &lhs,
184 const HashedStorage<StorageTy> &rhs) {
185 return lhs.storage == rhs.storage;
186 }
187 static inline bool isEqual(const StorageTy &lhs,
188 const HashedStorage<StorageTy> &rhs) {
189 if (!rhs.storage)
190 return false;
191
192 return lhs.isEqual(rhs.storage);
193 }
194};
195
196// Values with structural equivalence intended to be internalized.
197//===----------------------------------------------------------------------===//
198
199/// Base class for all storage objects that can be materialized as attributes.
200/// This provides a cache for the attribute that was materialized from this
201/// storage.
202struct CachableStorage {
203 TypedAttr attrCache;
204};
205
206/// Storage object for an '!rtg.set<T>'.
207struct SetStorage : CachableStorage {
208 static unsigned computeHash(const SetVector<ElaboratorValue> &set,
209 Type type) {
210 llvm::hash_code setHash = 0;
211 for (auto el : set) {
212 // Just XOR all hashes because it's a commutative operation and
213 // `llvm::hash_combine_range` is not commutative.
214 // We don't want the order in which elements were added to influence the
215 // hash and thus the equivalence of sets.
216 setHash = setHash ^ llvm::hash_combine(el);
217 }
218 return llvm::hash_combine(type, setHash);
219 }
220
221 SetStorage(SetVector<ElaboratorValue> &&set, Type type)
222 : hashcode(computeHash(set, type)), set(std::move(set)), type(type) {}
223
224 bool isEqual(const SetStorage *other) const {
225 // Note: we are not using the `==` operator of `SetVector` because it
226 // takes the order in which elements were added into account (since it's a
227 // vector after all). We just use it as a convenient way to keep track of a
228 // deterministic order for re-materialization.
229 bool allContained = true;
230 for (auto el : set)
231 allContained &= other->set.contains(el);
232
233 return hashcode == other->hashcode && set.size() == other->set.size() &&
234 allContained && type == other->type;
235 }
236
237 // The cached hashcode to avoid repeated computations.
238 const unsigned hashcode;
239
240 // Stores the elaborated values contained in the set.
241 const SetVector<ElaboratorValue> set;
242
243 // Store the set type such that we can materialize this evaluated value
244 // also in the case where the set is empty.
245 const Type type;
246};
247
248/// Storage object for an '!rtg.bag<T>'.
249struct BagStorage {
250 BagStorage(MapVector<ElaboratorValue, uint64_t> &&bag, Type type)
251 : hashcode(llvm::hash_combine(
252 type, llvm::hash_combine_range(bag.begin(), bag.end()))),
253 bag(std::move(bag)), type(type) {}
254
255 bool isEqual(const BagStorage *other) const {
256 return hashcode == other->hashcode && llvm::equal(bag, other->bag) &&
257 type == other->type;
258 }
259
260 // The cached hashcode to avoid repeated computations.
261 const unsigned hashcode;
262
263 // Stores the elaborated values contained in the bag with their number of
264 // occurences.
266
267 // Store the bag type such that we can materialize this evaluated value
268 // also in the case where the bag is empty.
269 const Type type;
270};
271
272/// Storage object for an '!rtg.sequence'.
273struct SequenceStorage {
274 SequenceStorage(StringAttr familyName, SmallVector<ElaboratorValue> &&args)
275 : hashcode(llvm::hash_combine(
276 familyName, llvm::hash_combine_range(args.begin(), args.end()))),
277 familyName(familyName), args(std::move(args)) {}
278
279 bool isEqual(const SequenceStorage *other) const {
280 return hashcode == other->hashcode && familyName == other->familyName &&
281 args == other->args;
282 }
283
284 // The cached hashcode to avoid repeated computations.
285 const unsigned hashcode;
286
287 // The name of the sequence family this sequence is derived from.
288 const StringAttr familyName;
289
290 // The elaborator values used during substitution of the sequence family.
291 const SmallVector<ElaboratorValue> args;
292};
293
294/// Storage object for interleaved '!rtg.randomized_sequence'es.
295struct InterleavedSequenceStorage {
296 InterleavedSequenceStorage(SmallVector<ElaboratorValue> &&sequences,
297 uint32_t batchSize)
298 : sequences(std::move(sequences)), batchSize(batchSize),
299 hashcode(llvm::hash_combine(
300 llvm::hash_combine_range(sequences.begin(), sequences.end()),
301 batchSize)) {}
302
303 explicit InterleavedSequenceStorage(RandomizedSequenceStorage *sequence)
304 : sequences(SmallVector<ElaboratorValue>(1, sequence)), batchSize(1),
305 hashcode(llvm::hash_combine(
306 llvm::hash_combine_range(sequences.begin(), sequences.end()),
307 batchSize)) {}
308
309 bool isEqual(const InterleavedSequenceStorage *other) const {
310 return hashcode == other->hashcode && sequences == other->sequences &&
311 batchSize == other->batchSize;
312 }
313
314 const SmallVector<ElaboratorValue> sequences;
315
316 const uint32_t batchSize;
317
318 // The cached hashcode to avoid repeated computations.
319 const unsigned hashcode;
320};
321
322/// Storage object for '!rtg.array`-typed values.
323struct ArrayStorage {
324 ArrayStorage(Type type, SmallVector<ElaboratorValue> &&array)
325 : hashcode(llvm::hash_combine(
326 type, llvm::hash_combine_range(array.begin(), array.end()))),
327 type(type), array(array) {}
328
329 bool isEqual(const ArrayStorage *other) const {
330 return hashcode == other->hashcode && type == other->type &&
331 array == other->array;
332 }
333
334 // The cached hashcode to avoid repeated computations.
335 const unsigned hashcode;
336
337 /// The type of the array. This is necessary because an array of size 0
338 /// cannot be reconstructed without knowing the original element type.
339 const Type type;
340
341 /// The label name. For unique labels, this is just the prefix.
342 const SmallVector<ElaboratorValue> array;
343};
344
345/// Storage object for 'tuple`-typed values.
346struct TupleStorage : CachableStorage {
347 TupleStorage(SmallVector<ElaboratorValue> &&values)
348 : hashcode(llvm::hash_combine_range(values.begin(), values.end())),
349 values(std::move(values)) {}
350
351 bool isEqual(const TupleStorage *other) const {
352 return hashcode == other->hashcode && values == other->values;
353 }
354
355 // The cached hashcode to avoid repeated computations.
356 const unsigned hashcode;
357
358 const SmallVector<ElaboratorValue> values;
359};
360
361struct SymbolicComputationStorage {
362 SymbolicComputationStorage(const DenseMap<Value, ElaboratorValue> &state,
363 Operation *op)
364 : name(op->getName()), resultTypes(op->getResultTypes()),
365 operands(llvm::map_range(op->getOperands(),
366 [&](Value v) { return state.lookup(v); })),
367 attributes(op->getAttrDictionary()),
368 properties(op->getPropertiesAsAttribute()),
369 hashcode(llvm::hash_combine(name, llvm::hash_combine_range(resultTypes),
370 llvm::hash_combine_range(operands),
371 attributes, op->hashProperties())) {}
372
373 bool isEqual(const SymbolicComputationStorage *other) const {
374 return hashcode == other->hashcode && name == other->name &&
375 resultTypes == other->resultTypes && operands == other->operands &&
376 attributes == other->attributes && properties == other->properties;
377 }
378
379 const OperationName name;
380 const SmallVector<Type> resultTypes;
381 const SmallVector<ElaboratorValue> operands;
382 const DictionaryAttr attributes;
383 const Attribute properties;
384 const unsigned hashcode;
385};
386
387// Values with identity not intended to be internalized.
388//===----------------------------------------------------------------------===//
389
390/// Base class for storages that represent values with identity, i.e., two
391/// values are not considered equivalent if they are structurally the same, but
392/// each definition of such a value is unique. E.g., unique labels or virtual
393/// registers. These cannot be materialized anew in each nested sequence, but
394/// must be passed as arguments.
395struct IdentityValue {
396
397 IdentityValue(Type type, Location loc) : type(type), loc(loc) {}
398
399#ifndef NDEBUG
400
401 /// In debug mode, track whether this value was already materialized to
402 /// assert if it's illegally materialized multiple times.
403 ///
404 /// Instead of deleting operations defining these values and materializing
405 /// them again, we could retain the operations. However, we still need
406 /// specific storages to represent these values in some cases, e.g., to get
407 /// the size of a memory allocation. Also, elaboration of nested control-flow
408 /// regions (e.g. `scf.for`) relies on materialization of such values lazily
409 /// instead of cloning the operations eagerly.
410 bool alreadyMaterialized = false;
411
412#endif
413
414 const Type type;
415 const Location loc;
416};
417
418/// Represents a unique virtual register.
419struct VirtualRegisterStorage : IdentityValue {
420 VirtualRegisterStorage(VirtualRegisterConfigAttr allowedRegs, Type type,
421 Location loc)
422 : IdentityValue(type, loc), allowedRegs(allowedRegs) {}
423
424 // NOTE: we don't need an 'isEqual' function and 'hashcode' here because
425 // VirtualRegisters are never internalized.
426
427 // The list of fixed registers allowed to be selected for this virtual
428 // register.
429 const VirtualRegisterConfigAttr allowedRegs;
430};
431
432struct UniqueLabelStorage : IdentityValue {
433 UniqueLabelStorage(const ElaboratorValue &name, Location loc)
434 : IdentityValue(LabelType::get(loc->getContext()), loc), name(name) {}
435
436 // NOTE: we don't need an 'isEqual' function and 'hashcode' here because
437 // VirtualRegisters are never internalized.
438
439 /// The label name. For unique labels, this is just the prefix.
440 const ElaboratorValue name;
441};
442
443/// Storage object for '!rtg.isa.memoryblock`-typed values.
444struct MemoryBlockStorage : IdentityValue {
445 MemoryBlockStorage(const APInt &baseAddress, const APInt &endAddress,
446 Type type, Location loc)
447 : IdentityValue(type, loc), baseAddress(baseAddress),
448 endAddress(endAddress) {}
449
450 // The base address of the memory. The width of the APInt also represents the
451 // address width of the memory. This is an APInt to support memories of
452 // >64-bit machines.
453 const APInt baseAddress;
454
455 // The last address of the memory.
456 const APInt endAddress;
457};
458
459/// Storage object for '!rtg.isa.memory`-typed values.
460struct MemoryStorage : IdentityValue {
461 MemoryStorage(MemoryBlockStorage *memoryBlock, size_t size, size_t alignment,
462 Location loc)
463 : IdentityValue(MemoryType::get(memoryBlock->type.getContext(),
464 memoryBlock->baseAddress.getBitWidth()),
465 loc),
466 memoryBlock(memoryBlock), size(size), alignment(alignment) {}
467
468 MemoryBlockStorage *memoryBlock;
469 const size_t size;
470 const size_t alignment;
471};
472
473/// Storage object for an '!rtg.randomized_sequence'.
474struct RandomizedSequenceStorage : IdentityValue {
475 RandomizedSequenceStorage(ContextResourceAttrInterface context,
476 SequenceStorage *sequence, Location loc)
477 : IdentityValue(
478 RandomizedSequenceType::get(sequence->familyName.getContext()),
479 loc),
480 context(context), sequence(sequence) {}
481
482 // The context under which this sequence is placed.
483 const ContextResourceAttrInterface context;
484
485 const SequenceStorage *sequence;
486};
487
488/// Operation must have at least 1 result.
489struct SymbolicComputationWithIdentityStorage : IdentityValue {
490 SymbolicComputationWithIdentityStorage(
491 const DenseMap<Value, ElaboratorValue> &state, Operation *op)
492 : IdentityValue(op->getResult(0).getType(), op->getLoc()),
493 name(op->getName()), resultTypes(op->getResultTypes()),
494 operands(llvm::map_range(op->getOperands(),
495 [&](Value v) { return state.lookup(v); })),
496 attributes(op->getAttrDictionary()),
497 properties(op->getPropertiesAsAttribute()) {}
498
499 const OperationName name;
500 const SmallVector<Type> resultTypes;
501 const SmallVector<ElaboratorValue> operands;
502 const DictionaryAttr attributes;
503 const Attribute properties;
504};
505
506struct SymbolicComputationWithIdentityValue : IdentityValue {
507 SymbolicComputationWithIdentityValue(
508 Type type, const SymbolicComputationWithIdentityStorage *storage,
509 unsigned idx)
510 : IdentityValue(type, storage->loc), storage(storage), idx(idx) {
511 assert(
512 idx != 0 &&
513 "Use SymbolicComputationWithIdentityStorage for result with index 0.");
514 }
515
516 const SymbolicComputationWithIdentityStorage *storage;
517 const unsigned idx;
518};
519
520/// Storage for SSA values produced by external ops with regions — block
521/// arguments and results. Carries identity only; the corresponding new-IR
522/// `Value` is registered via `Materializer::map()` immediately upon creation so
523/// `Materializer::materialize()` always hits its cache and never dispatches
524// through `visit()`.
525struct OpaqueExternalStorage : IdentityValue {
526 OpaqueExternalStorage(Type type, Location loc) : IdentityValue(type, loc) {}
527};
528
529/// Maps effect names to their handler regions within a single rtg.handle op.
530struct HandlerFrame {
531 DenseMap<StringAttr, Region *> handlers;
532};
533
534/// Storage object for '!rtg.continuation<T>' — a captured elaboration frame.
535/// Holds the ops to elaborate on resume and the handler-stack snapshot.
536struct ContinuationStorage : IdentityValue {
537 ContinuationStorage(SmallVector<Operation *> remainingOps,
538 Value performResult,
539 SmallVector<HandlerFrame> capturedHandlerStack,
540 Type resumeType, Location loc)
541 : IdentityValue(ContinuationType::get(loc.getContext(), resumeType), loc),
542 remainingOps(std::move(remainingOps)), performResult(performResult),
543 capturedHandlerStack(std::move(capturedHandlerStack)) {}
544
545 /// Ops from the HandleOp body that appear after the rtg.perform.
546 SmallVector<Operation *> remainingOps;
547 /// The SSA result of the rtg.perform op (null for unit-result effects).
548 Value performResult;
549 /// Handler-stack snapshot at the point the perform was triggered.
550 SmallVector<HandlerFrame> capturedHandlerStack;
551};
552
553/// An 'Internalizer' object internalizes storages and takes ownership of them.
554/// When the initializer object is destroyed, all owned storages are also
555/// deallocated and thus must not be accessed anymore.
556class Internalizer {
557public:
558 /// Internalize a storage of type `StorageTy` constructed with arguments
559 /// `args`. The pointers returned by this method can be used to compare
560 /// objects when, e.g., computing set differences, uniquing the elements in a
561 /// set, etc. Otherwise, we'd need to do a deep value comparison in those
562 /// situations.
563 template <typename StorageTy, typename... Args>
564 StorageTy *internalize(Args &&...args) {
565 static_assert(!std::is_base_of_v<IdentityValue, StorageTy> &&
566 "values with identity must not be internalized");
567
568 StorageTy storage(std::forward<Args>(args)...);
569
570 auto existing = getInternSet<StorageTy>().insert_as(
571 HashedStorage<StorageTy>(storage.hashcode), storage);
572 StorageTy *&storagePtr = existing.first->storage;
573 if (existing.second)
574 storagePtr =
575 new (allocator.Allocate<StorageTy>()) StorageTy(std::move(storage));
576
577 return storagePtr;
578 }
579
580 template <typename StorageTy, typename... Args>
581 StorageTy *create(Args &&...args) {
582 static_assert(std::is_base_of_v<IdentityValue, StorageTy> &&
583 "values with structural equivalence must be internalized");
584
585 return new (allocator.Allocate<StorageTy>())
586 StorageTy(std::forward<Args>(args)...);
587 }
588
589private:
590 template <typename StorageTy>
591 DenseSet<HashedStorage<StorageTy>, StorageKeyInfo<StorageTy>> &
592 getInternSet() {
593 if constexpr (std::is_same_v<StorageTy, ArrayStorage>)
594 return internedArrays;
595 else if constexpr (std::is_same_v<StorageTy, SetStorage>)
596 return internedSets;
597 else if constexpr (std::is_same_v<StorageTy, BagStorage>)
598 return internedBags;
599 else if constexpr (std::is_same_v<StorageTy, SequenceStorage>)
600 return internedSequences;
601 else if constexpr (std::is_same_v<StorageTy, RandomizedSequenceStorage>)
602 return internedRandomizedSequences;
603 else if constexpr (std::is_same_v<StorageTy, InterleavedSequenceStorage>)
604 return internedInterleavedSequences;
605 else if constexpr (std::is_same_v<StorageTy, TupleStorage>)
606 return internedTuples;
607 else if constexpr (std::is_same_v<StorageTy, SymbolicComputationStorage>)
608 return internedSymbolicComputationWithIdentityValues;
609 else
610 static_assert(!sizeof(StorageTy),
611 "no intern set available for this storage type.");
612 }
613
614 // This allocator allocates on the heap. It automatically deallocates all
615 // objects it allocated once the allocator itself is destroyed.
616 llvm::BumpPtrAllocator allocator;
617
618 // The sets holding the internalized objects. We use one set per storage type
619 // such that we can have a simpler equality checking function (no need to
620 // compare some sort of TypeIDs).
621 DenseSet<HashedStorage<ArrayStorage>, StorageKeyInfo<ArrayStorage>>
622 internedArrays;
623 DenseSet<HashedStorage<SetStorage>, StorageKeyInfo<SetStorage>> internedSets;
624 DenseSet<HashedStorage<BagStorage>, StorageKeyInfo<BagStorage>> internedBags;
625 DenseSet<HashedStorage<SequenceStorage>, StorageKeyInfo<SequenceStorage>>
626 internedSequences;
627 DenseSet<HashedStorage<RandomizedSequenceStorage>,
628 StorageKeyInfo<RandomizedSequenceStorage>>
629 internedRandomizedSequences;
630 DenseSet<HashedStorage<InterleavedSequenceStorage>,
631 StorageKeyInfo<InterleavedSequenceStorage>>
632 internedInterleavedSequences;
633 DenseSet<HashedStorage<TupleStorage>, StorageKeyInfo<TupleStorage>>
634 internedTuples;
635 DenseSet<HashedStorage<SymbolicComputationStorage>,
636 StorageKeyInfo<SymbolicComputationStorage>>
637 internedSymbolicComputationWithIdentityValues;
638};
639
640} // namespace
641
642#ifndef NDEBUG
643
644static llvm::raw_ostream &operator<<(llvm::raw_ostream &os,
645 const ElaboratorValue &value);
646
647static void print(TypedAttr val, llvm::raw_ostream &os) {
648 os << "<attr " << val << ">";
649}
650
651static void print(BagStorage *val, llvm::raw_ostream &os) {
652 os << "<bag {";
653 llvm::interleaveComma(val->bag, os,
654 [&](const std::pair<ElaboratorValue, uint64_t> &el) {
655 os << el.first << " -> " << el.second;
656 });
657 os << "} at " << val << ">";
658}
659
660static void print(bool val, llvm::raw_ostream &os) {
661 os << "<bool " << (val ? "true" : "false") << ">";
662}
663
664static void print(size_t val, llvm::raw_ostream &os) {
665 os << "<index " << val << ">";
666}
667
668static void print(SequenceStorage *val, llvm::raw_ostream &os) {
669 os << "<sequence @" << val->familyName.getValue() << "(";
670 llvm::interleaveComma(val->args, os,
671 [&](const ElaboratorValue &val) { os << val; });
672 os << ") at " << val << ">";
673}
674
675static void print(RandomizedSequenceStorage *val, llvm::raw_ostream &os) {
676 os << "<randomized-sequence derived from @"
677 << val->sequence->familyName.getValue() << " under context "
678 << val->context << "(";
679 llvm::interleaveComma(val->sequence->args, os,
680 [&](const ElaboratorValue &val) { os << val; });
681 os << ") at " << val << ">";
682}
683
684static void print(InterleavedSequenceStorage *val, llvm::raw_ostream &os) {
685 os << "<interleaved-sequence [";
686 llvm::interleaveComma(val->sequences, os,
687 [&](const ElaboratorValue &val) { os << val; });
688 os << "] batch-size " << val->batchSize << " at " << val << ">";
689}
690
691static void print(ArrayStorage *val, llvm::raw_ostream &os) {
692 os << "<array [";
693 llvm::interleaveComma(val->array, os,
694 [&](const ElaboratorValue &val) { os << val; });
695 os << "] at " << val << ">";
696}
697
698static void print(SetStorage *val, llvm::raw_ostream &os) {
699 os << "<set {";
700 llvm::interleaveComma(val->set, os,
701 [&](const ElaboratorValue &val) { os << val; });
702 os << "} at " << val << ">";
703}
704
705static void print(const VirtualRegisterStorage *val, llvm::raw_ostream &os) {
706 os << "<virtual-register " << val << " " << val->allowedRegs << ">";
707}
708
709static void print(const UniqueLabelStorage *val, llvm::raw_ostream &os) {
710 os << "<unique-label " << val << " " << val->name << ">";
711}
712
713static void print(const TupleStorage *val, llvm::raw_ostream &os) {
714 os << "<tuple (";
715 llvm::interleaveComma(val->values, os,
716 [&](const ElaboratorValue &val) { os << val; });
717 os << ")>";
718}
719
720static void print(const MemoryStorage *val, llvm::raw_ostream &os) {
721 os << "<memory {" << ElaboratorValue(val->memoryBlock)
722 << ", size=" << val->size << ", alignment=" << val->alignment << "}>";
723}
724
725static void print(const MemoryBlockStorage *val, llvm::raw_ostream &os) {
726 os << "<memory-block {"
727 << ", address-width=" << val->baseAddress.getBitWidth()
728 << ", base-address=" << val->baseAddress
729 << ", end-address=" << val->endAddress << "}>";
730}
731
732static void print(const SymbolicComputationWithIdentityValue *val,
733 llvm::raw_ostream &os) {
734 os << "<symbolic-computation-with-identity-value (" << val->storage << ") at "
735 << val->idx << ">";
736}
737
738static void print(const SymbolicComputationWithIdentityStorage *val,
739 llvm::raw_ostream &os) {
740 os << "<symbolic-computation-with-identity " << val->name << "(";
741 llvm::interleaveComma(val->operands, os,
742 [&](const ElaboratorValue &val) { os << val; });
743 os << ") -> " << val->resultTypes << " with attributes " << val->attributes
744 << " and properties " << val->properties;
745 os << ">";
746}
747
748static void print(const SymbolicComputationStorage *val,
749 llvm::raw_ostream &os) {
750 os << "<symbolic-computation " << val->name << "(";
751 llvm::interleaveComma(val->operands, os,
752 [&](const ElaboratorValue &val) { os << val; });
753 os << ") -> " << val->resultTypes << " with attributes " << val->attributes
754 << " and properties " << val->properties;
755 os << ">";
756}
757
758static void print(const OpaqueExternalStorage *val, llvm::raw_ostream &os) {
759 os << "<opaque-external " << val->type << ">";
760}
761
762static void print(const ContinuationStorage *val, llvm::raw_ostream &os) {
763 os << "<continuation with " << val->remainingOps.size() << " ops at " << val
764 << ">";
765}
766
767static llvm::raw_ostream &operator<<(llvm::raw_ostream &os,
768 const ElaboratorValue &value) {
769 std::visit([&](auto val) { print(val, os); }, value);
770
771 return os;
772}
773
774#endif
775
776//===----------------------------------------------------------------------===//
777// Attribute <-> ElaboratorValue Converters
778//===----------------------------------------------------------------------===//
779
780namespace {
781
782/// Convert an attribute to an ElaboratorValue. This handles nested attributes
783/// like SetAttr and TupleAttr recursively.
784class AttributeToElaboratorValueConverter {
785public:
786 AttributeToElaboratorValueConverter(Internalizer &internalizer)
787 : internalizer(internalizer) {}
788
789 /// Convert an attribute to an ElaboratorValue.
790 FailureOr<ElaboratorValue> convert(Attribute attr) {
791 return llvm::TypeSwitch<Attribute, FailureOr<ElaboratorValue>>(attr)
792 .Case<IntegerAttr, SetAttr, TupleAttr>(
793 [&](auto attr) { return convert(attr); })
794 .Case<TypedAttr>([&](auto typedAttr) -> FailureOr<ElaboratorValue> {
795 return ElaboratorValue(typedAttr);
796 })
797 .Default(
798 [&](Attribute) -> FailureOr<ElaboratorValue> { return failure(); });
799 }
800
801private:
802 FailureOr<ElaboratorValue> convert(IntegerAttr attr) {
803 if (attr.getType().isSignlessInteger(1))
804 return ElaboratorValue(bool(attr.getInt()));
805 if (isa<IndexType>(attr.getType()))
806 return ElaboratorValue(size_t(attr.getInt()));
807 return ElaboratorValue(attr);
808 }
809
810 FailureOr<ElaboratorValue> convert(SetAttr setAttr) {
811 SetVector<ElaboratorValue> set;
812 for (auto element : *setAttr.getElements()) {
813 auto converted = convert(element);
814 if (failed(converted))
815 return failure();
816 set.insert(*converted);
817 }
818 auto *storage =
819 internalizer.internalize<SetStorage>(std::move(set), setAttr.getType());
820 // Cache the original attribute for efficient materialization
821 storage->attrCache = setAttr;
822 return ElaboratorValue(storage);
823 }
824
825 FailureOr<ElaboratorValue> convert(TupleAttr tupleAttr) {
826 SmallVector<ElaboratorValue> values;
827 for (auto element : tupleAttr.getElements()) {
828 auto converted = convert(element);
829 if (failed(converted))
830 return failure();
831 values.push_back(*converted);
832 }
833 auto *storage = internalizer.internalize<TupleStorage>(std::move(values));
834 // Cache the original attribute for efficient materialization
835 storage->attrCache = tupleAttr;
836 return ElaboratorValue(storage);
837 }
838
839 Internalizer &internalizer;
840};
841
842/// Convert an ElaboratorValue to an attribute when possible. Not all
843/// ElaboratorValues can be converted to attributes (e.g., values with
844/// identity).
845class ElaboratorValueToAttributeConverter {
846public:
847 ElaboratorValueToAttributeConverter(MLIRContext *context)
848 : context(context) {}
849
850 /// Convert an ElaboratorValue to an attribute. Returns a null attribute if
851 /// the conversion is not possible.
852 TypedAttr convert(const ElaboratorValue &value) {
853 return std::visit(
854 [&](auto val) -> TypedAttr {
855 if constexpr (std::is_base_of_v<CachableStorage,
856 std::remove_pointer_t<
857 std::decay_t<decltype(value)>>>) {
858 if (val->attrCache)
859 return val->attrCache;
860 }
861 return visit(val);
862 },
863 value);
864 }
865
866private:
867 TypedAttr visit(TypedAttr val) { return val; }
868
869 TypedAttr visit(bool val) {
870 return IntegerAttr::get(IntegerType::get(context, 1), val);
871 }
872
873 TypedAttr visit(size_t val) {
874 return IntegerAttr::get(IndexType::get(context), val);
875 }
876
877 TypedAttr visit(SetStorage *val) {
878 DenseSet<TypedAttr> elements;
879 for (auto element : val->set) {
880 auto converted = convert(element);
881 if (!converted)
882 return {};
883 auto typedAttr = dyn_cast<TypedAttr>(converted);
884 if (!typedAttr)
885 return {};
886 elements.insert(typedAttr);
887 }
888 return SetAttr::get(cast<SetType>(val->type), &elements);
889 }
890
891 TypedAttr visit(TupleStorage *val) {
892 SmallVector<TypedAttr> elements;
893 for (auto element : val->values) {
894 auto converted = convert(element);
895 if (!converted)
896 return {};
897 auto typedAttr = dyn_cast<TypedAttr>(converted);
898 if (!typedAttr)
899 return {};
900 elements.push_back(typedAttr);
901 }
902 return TupleAttr::get(context, elements);
903 }
904
905 // Default implementation for storage types that cannot be converted to
906 // attributes. Using a macro to avoid repetition. std::visit does not support
907 // any kind of "default" clauses, unfortunately.
908#define VISIT_UNSUPPORTED(STORAGETYPE) \
909 /* NOLINTNEXTLINE(bugprone-macro-parentheses)*/ \
910 TypedAttr visit(STORAGETYPE *val) { return {}; }
911
912 VISIT_UNSUPPORTED(ArrayStorage)
913 VISIT_UNSUPPORTED(BagStorage)
914 VISIT_UNSUPPORTED(SequenceStorage)
915 VISIT_UNSUPPORTED(RandomizedSequenceStorage)
916 VISIT_UNSUPPORTED(InterleavedSequenceStorage)
917 VISIT_UNSUPPORTED(VirtualRegisterStorage)
918 VISIT_UNSUPPORTED(UniqueLabelStorage)
919 VISIT_UNSUPPORTED(MemoryStorage)
920 VISIT_UNSUPPORTED(MemoryBlockStorage)
921 VISIT_UNSUPPORTED(SymbolicComputationWithIdentityStorage)
922 VISIT_UNSUPPORTED(SymbolicComputationWithIdentityValue)
923 VISIT_UNSUPPORTED(SymbolicComputationStorage)
924 VISIT_UNSUPPORTED(OpaqueExternalStorage)
925 VISIT_UNSUPPORTED(ContinuationStorage)
926
927#undef VISIT_UNSUPPORTED
928
929 MLIRContext *context;
930};
931
932} // namespace
933
934//===----------------------------------------------------------------------===//
935// Elaborator Value Materialization
936//===----------------------------------------------------------------------===//
937
938namespace {
939
940/// State that should be shared by all elaborator and materializer instances.
941struct SharedState {
942 SharedState(MLIRContext *ctxt, SymbolTable &table)
943 : ctxt(ctxt), table(table) {}
944
945 MLIRContext *ctxt;
946 SymbolTable &table;
947 Namespace names;
948 Internalizer internalizer;
949};
950
951/// A collection of state per RTG test.
952struct TestState {
953 explicit TestState(unsigned seed) : rng(RngScope(seed)) {}
954
955 /// The name of the test.
956 StringAttr name;
957
958 /// The context switches registered for this test.
959 MapVector<
960 std::pair<ContextResourceAttrInterface, ContextResourceAttrInterface>,
961 SequenceStorage *>
962 contextSwitches;
963
964 /// The root RNG scope for this test.
965 RngScope rng;
966};
967
968/// Construct an SSA value from a given elaborated value.
969class Materializer {
970public:
971 Materializer(OpBuilder builder, TestState &testState,
972 SharedState &sharedState,
973 SmallVector<ElaboratorValue> &blockArgs)
974 : builder(builder), rootBlock(builder.getBlock()), testState(testState),
975 sharedState(sharedState), blockArgs(blockArgs),
976 attrConverter(builder.getContext()) {}
977
978 /// Materialize IR representing the provided `ElaboratorValue` and return the
979 /// `Value` or a null value on failure.
980 Value materialize(ElaboratorValue val, Location loc,
981 function_ref<InFlightDiagnostic()> emitError) {
982 auto iter = materializedValues.find(val);
983 if (iter != materializedValues.end())
984 return iter->second;
985
986 LLVM_DEBUG(llvm::dbgs() << "Materializing " << val);
987
988 if (auto res = tryMaterializeAsConstant(val, loc))
989 return res;
990
991 // In debug mode, track whether values with identity were already
992 // materialized before and assert in such a situation.
993 Value res = std::visit(
994 [&](auto value) {
995 if constexpr (std::is_base_of_v<IdentityValue,
996 std::remove_pointer_t<
997 std::decay_t<decltype(value)>>>) {
998 if (identityValueRoot.contains(value)) {
999#ifndef NDEBUG
1000 bool &materialized =
1001 static_cast<IdentityValue *>(value)->alreadyMaterialized;
1002 assert(!materialized && "must not already be materialized");
1003 materialized = true;
1004#endif
1005
1006 return visit(value, loc, emitError);
1007 }
1008
1009 Value arg = builder.getBlock()->addArgument(value->type, loc);
1010 blockArgs.push_back(val);
1011 blockArgTypes.push_back(arg.getType());
1012 materializedValues[val] = arg;
1013 return arg;
1014 }
1015
1016 return visit(value, loc, emitError);
1017 },
1018 val);
1019
1020 LLVM_DEBUG(llvm::dbgs() << " to\n" << res << "\n\n");
1021
1022 return res;
1023 }
1024
1025 bool isInPlace(Operation *op) const {
1026 return builder.getBlock()->getParent() == op->getParentRegion();
1027 }
1028
1029 /// If `op` is not in the same region as the materializer insertion point, a
1030 /// clone is created at the materializer's insertion point by also
1031 /// materializing the `ElaboratorValue`s for each operand just before it.
1032 /// Otherwise, all operations after the materializer's insertion point are
1033 /// deleted until `op` is reached. An error is returned if the operation is
1034 /// before the insertion point.
1035 LogicalResult materialize(Operation *op,
1036 DenseMap<Value, ElaboratorValue> &state) {
1037 // Region-bearing ops must be elaborated away before reaching here (either
1038 // by a dedicated visitor, or by visitExternalOp which performs its own
1039 // structural rewrite). Routing them through this generic operand-only
1040 // materialize path would silently keep un-elaborated regions in the
1041 // output.
1042 if (op->getNumRegions() > 0)
1043 return op->emitOpError("ops with nested regions must be elaborated away");
1044
1045 // We don't support opaque values. If there is an SSA value that has a
1046 // use-site it needs an equivalent ElaborationValue representation.
1047 // NOTE: We could support cases where there is initially a use-site but that
1048 // op is guaranteed to be deleted during elaboration. Or the use-sites are
1049 // replaced with freshly materialized values from the ElaborationValue. But
1050 // then, why can't we delete the value defining op?
1051 for (auto res : op->getResults())
1052 if (!res.use_empty())
1053 return op->emitOpError(
1054 "ops with results that have uses are not supported");
1055
1056 if (isInPlace(op)) {
1057 // We are doing in-place materialization, so mark all ops deleted until we
1058 // reach the one to be materialized and modify it in-place.
1059 deleteOpsUntil([&](auto iter) { return &*iter == op; });
1060
1061 if (builder.getInsertionPoint() == builder.getBlock()->end())
1062 return op->emitError("operation did not occur after the current "
1063 "materializer insertion point");
1064
1065 LLVM_DEBUG(llvm::dbgs() << "Modifying in-place: " << *op << "\n\n");
1066 } else {
1067 LLVM_DEBUG(llvm::dbgs() << "Materializing a clone of " << *op << "\n\n");
1068 op = builder.clone(*op);
1069 builder.setInsertionPoint(op);
1070 }
1071
1072 for (auto &operand : op->getOpOperands()) {
1073 auto emitError = [&]() {
1074 auto diag = op->emitError();
1075 diag.attachNote(op->getLoc())
1076 << "while materializing value for operand#"
1077 << operand.getOperandNumber();
1078 return diag;
1079 };
1080
1081 auto elabVal = state.at(operand.get());
1082 Value val = materialize(elabVal, op->getLoc(), emitError);
1083 if (!val)
1084 return failure();
1085
1086 state[val] = elabVal;
1087 operand.set(val);
1088 }
1089
1090 builder.setInsertionPointAfter(op);
1091 return success();
1092 }
1093
1094 /// Should be called once the `Region` is successfully materialized. No calls
1095 /// to `materialize` should happen after this anymore.
1096 void finalize() {
1097 deleteOpsUntil([](auto iter) { return false; });
1098
1099 for (auto *op : llvm::reverse(toDelete))
1100 op->erase();
1101 }
1102
1103 /// Tell this materializer that it is responsible for materializing the given
1104 /// identity value at the earliest position it is needed, and should't
1105 /// request the value via block argument.
1106 void registerIdentityValue(IdentityValue *val) {
1107 identityValueRoot.insert(val);
1108 }
1109
1110 ArrayRef<Type> getBlockArgTypes() const { return blockArgTypes; }
1111
1112 void map(ElaboratorValue eval, Value val) { materializedValues[eval] = val; }
1113
1114 OpBuilder &getBuilder() { return builder; }
1115
1116 template <typename OpTy, typename... Args>
1117 OpTy create(Location location, Args &&...args) {
1118 return OpTy::create(builder, location, std::forward<Args>(args)...);
1119 }
1120
1121private:
1122 Value tryMaterializeAsConstant(ElaboratorValue val, Location loc) {
1123 if (auto attr = attrConverter.convert(val)) {
1124 // Hoist constants to the materializer's root block when the builder's
1125 // current insertion point is in a nested region. This ensures the
1126 // constant's defining region dominates every potential use, including
1127 // sibling regions that share this materializer's value cache.
1128 OpBuilder::InsertionGuard guard(builder);
1129 if (builder.getBlock() != rootBlock)
1130 builder.setInsertionPointToStart(rootBlock);
1131 Value res = ConstantOp::create(builder, loc, attr);
1132 materializedValues[val] = res;
1133 return res;
1134 }
1135
1136 return Value();
1137 }
1138
1139 SequenceOp elaborateSequence(const RandomizedSequenceStorage *seq,
1140 SmallVector<ElaboratorValue> &elabArgs);
1141
1142 void deleteOpsUntil(function_ref<bool(Block::iterator)> stop) {
1143 auto ip = builder.getInsertionPoint();
1144 while (ip != builder.getBlock()->end() && !stop(ip)) {
1145 LLVM_DEBUG(llvm::dbgs() << "Marking to be deleted: " << *ip << "\n\n");
1146 toDelete.push_back(&*ip);
1147
1148 builder.setInsertionPointAfter(&*ip);
1149 ip = builder.getInsertionPoint();
1150 }
1151 }
1152
1153 Value visit(TypedAttr val, Location loc,
1154 function_ref<InFlightDiagnostic()> emitError) {
1155 return {};
1156 }
1157
1158 Value visit(size_t val, Location loc,
1159 function_ref<InFlightDiagnostic()> emitError) {
1160 return {};
1161 }
1162
1163 Value visit(bool val, Location loc,
1164 function_ref<InFlightDiagnostic()> emitError) {
1165 return {};
1166 }
1167
1168 Value visit(ArrayStorage *val, Location loc,
1169 function_ref<InFlightDiagnostic()> emitError) {
1170 SmallVector<Value> elements;
1171 elements.reserve(val->array.size());
1172 for (auto el : val->array) {
1173 auto materialized = materialize(el, loc, emitError);
1174 if (!materialized)
1175 return Value();
1176
1177 elements.push_back(materialized);
1178 }
1179
1180 Value res = ArrayCreateOp::create(builder, loc, val->type, elements);
1181 materializedValues[val] = res;
1182 return res;
1183 }
1184
1185 Value visit(SetStorage *val, Location loc,
1186 function_ref<InFlightDiagnostic()> emitError) {
1187 SmallVector<Value> elements;
1188 elements.reserve(val->set.size());
1189 for (auto el : val->set) {
1190 auto materialized = materialize(el, loc, emitError);
1191 if (!materialized)
1192 return Value();
1193
1194 elements.push_back(materialized);
1195 }
1196
1197 auto res = SetCreateOp::create(builder, loc, val->type, elements);
1198 materializedValues[val] = res;
1199 return res;
1200 }
1201
1202 Value visit(BagStorage *val, Location loc,
1203 function_ref<InFlightDiagnostic()> emitError) {
1204 SmallVector<Value> values, weights;
1205 values.reserve(val->bag.size());
1206 weights.reserve(val->bag.size());
1207 for (auto [val, weight] : val->bag) {
1208 auto materializedVal = materialize(val, loc, emitError);
1209 auto materializedWeight = materialize(weight, loc, emitError);
1210 if (!materializedVal || !materializedWeight)
1211 return Value();
1212
1213 values.push_back(materializedVal);
1214 weights.push_back(materializedWeight);
1215 }
1216
1217 auto res = BagCreateOp::create(builder, loc, val->type, values, weights);
1218 materializedValues[val] = res;
1219 return res;
1220 }
1221
1222 Value visit(MemoryBlockStorage *val, Location loc,
1223 function_ref<InFlightDiagnostic()> emitError) {
1224 auto intType = builder.getIntegerType(val->baseAddress.getBitWidth());
1225 Value res = MemoryBlockDeclareOp::create(
1226 builder, val->loc, val->type,
1227 IntegerAttr::get(intType, val->baseAddress),
1228 IntegerAttr::get(intType, val->endAddress));
1229 materializedValues[val] = res;
1230 return res;
1231 }
1232
1233 Value visit(MemoryStorage *val, Location loc,
1234 function_ref<InFlightDiagnostic()> emitError) {
1235 auto memBlock = materialize(val->memoryBlock, val->loc, emitError);
1236 auto memSize = materialize(val->size, val->loc, emitError);
1237 auto memAlign = materialize(val->alignment, val->loc, emitError);
1238 if (!(memBlock && memSize && memAlign))
1239 return {};
1240
1241 Value res =
1242 MemoryAllocOp::create(builder, val->loc, memBlock, memSize, memAlign);
1243 materializedValues[val] = res;
1244 return res;
1245 }
1246
1247 Value visit(SequenceStorage *val, Location loc,
1248 function_ref<InFlightDiagnostic()> emitError) {
1249 emitError() << "materializing a non-randomized sequence not supported yet";
1250 return Value();
1251 }
1252
1253 Value visit(RandomizedSequenceStorage *val, Location loc,
1254 function_ref<InFlightDiagnostic()> emitError) {
1255 // To know which values we have to pass by argument (and not just pass all
1256 // that migth be used eagerly), we have to elaborate the sequence family if
1257 // not already done so.
1258 // We need to get back the sequence to reference, and the list of elaborated
1259 // values to pass as arguments.
1260 SmallVector<ElaboratorValue> elabArgs;
1261 // NOTE: we wouldn't need to elaborate the sequence if it doesn't contain
1262 // randomness to be elaborated.
1263 SequenceOp seqOp = elaborateSequence(val, elabArgs);
1264 if (!seqOp)
1265 return {};
1266
1267 // Materialize all the values we need to pass as arguments and collect their
1268 // types.
1269 SmallVector<Value> args;
1270 SmallVector<Type> argTypes;
1271 for (auto arg : elabArgs) {
1272 Value materialized = materialize(arg, val->loc, emitError);
1273 if (!materialized)
1274 return {};
1275
1276 args.push_back(materialized);
1277 argTypes.push_back(materialized.getType());
1278 }
1279
1280 Value res = GetSequenceOp::create(
1281 builder, val->loc, SequenceType::get(builder.getContext(), argTypes),
1282 seqOp.getSymName());
1283
1284 // Only materialize a substitute_sequence op when we have arguments to
1285 // substitute since this op does not support 0 arguments.
1286 if (!args.empty())
1287 res = SubstituteSequenceOp::create(builder, val->loc, res, args);
1288
1289 res = RandomizeSequenceOp::create(builder, val->loc, res);
1290
1291 materializedValues[val] = res;
1292 return res;
1293 }
1294
1295 Value visit(InterleavedSequenceStorage *val, Location loc,
1296 function_ref<InFlightDiagnostic()> emitError) {
1297 SmallVector<Value> sequences;
1298 for (auto seqVal : val->sequences) {
1299 Value materialized = materialize(seqVal, loc, emitError);
1300 if (!materialized)
1301 return {};
1302
1303 sequences.push_back(materialized);
1304 }
1305
1306 if (sequences.size() == 1)
1307 return sequences[0];
1308
1309 Value res =
1310 InterleaveSequencesOp::create(builder, loc, sequences, val->batchSize);
1311 materializedValues[val] = res;
1312 return res;
1313 }
1314
1315 Value visit(VirtualRegisterStorage *val, Location loc,
1316 function_ref<InFlightDiagnostic()> emitError) {
1317 Value res = VirtualRegisterOp::create(builder, val->loc, val->allowedRegs);
1318 materializedValues[val] = res;
1319 return res;
1320 }
1321
1322 Value visit(UniqueLabelStorage *val, Location loc,
1323 function_ref<InFlightDiagnostic()> emitError) {
1324 auto materialized = materialize(val->name, val->loc, emitError);
1325 if (!materialized)
1326 return {};
1327 Value res = LabelUniqueDeclOp::create(builder, val->loc, materialized);
1328 materializedValues[val] = res;
1329 return res;
1330 }
1331
1332 Value visit(TupleStorage *val, Location loc,
1333 function_ref<InFlightDiagnostic()> emitError) {
1334 SmallVector<Value> materialized;
1335 materialized.reserve(val->values.size());
1336 for (auto v : val->values)
1337 materialized.push_back(materialize(v, loc, emitError));
1338 Value res = TupleCreateOp::create(builder, loc, materialized);
1339 materializedValues[val] = res;
1340 return res;
1341 }
1342
1343 Value visit(ContinuationStorage *val, Location loc,
1344 function_ref<InFlightDiagnostic()> emitError) {
1345 emitError() << "continuation cannot be materialized into IR";
1346 return {};
1347 }
1348
1349 Value visit(SymbolicComputationWithIdentityValue *val, Location loc,
1350 function_ref<InFlightDiagnostic()> emitError) {
1351 auto *noConstStorage =
1352 const_cast<SymbolicComputationWithIdentityStorage *>(val->storage);
1353 auto res0 = materialize(noConstStorage, loc, emitError);
1354 if (!res0)
1355 return {};
1356
1357 auto *op = res0.getDefiningOp();
1358 auto res = op->getResults()[val->idx];
1359 materializedValues[val] = res;
1360 return res;
1361 }
1362
1363 Value visit(SymbolicComputationWithIdentityStorage *val, Location loc,
1364 function_ref<InFlightDiagnostic()> emitError) {
1365 SmallVector<Value> operands;
1366 for (auto operand : val->operands) {
1367 auto materialized = materialize(operand, val->loc, emitError);
1368 if (!materialized)
1369 return {};
1370
1371 operands.push_back(materialized);
1372 }
1373
1374 OperationState state(val->loc, val->name);
1375 state.addTypes(val->resultTypes);
1376 state.attributes = val->attributes;
1377 state.propertiesAttr = val->properties;
1378 state.addOperands(operands);
1379 auto *op = builder.create(state);
1380
1381 materializedValues[val] = op->getResult(0);
1382 return op->getResult(0);
1383 }
1384
1385 Value visit(SymbolicComputationStorage *val, Location loc,
1386 function_ref<InFlightDiagnostic()> emitError) {
1387 SmallVector<Value> operands;
1388 for (auto operand : val->operands) {
1389 auto materialized = materialize(operand, loc, emitError);
1390 if (!materialized)
1391 return {};
1392
1393 operands.push_back(materialized);
1394 }
1395
1396 OperationState state(loc, val->name);
1397 state.addTypes(val->resultTypes);
1398 state.attributes = val->attributes;
1399 state.propertiesAttr = val->properties;
1400 state.addOperands(operands);
1401 auto *op = builder.create(state);
1402
1403 for (auto res : op->getResults())
1404 materializedValues[val] = res;
1405
1406 return op->getResult(0);
1407 }
1408
1409 Value visit(OpaqueExternalStorage *val, Location loc,
1410 function_ref<InFlightDiagnostic()> emitError) {
1411 // Opaque external values (block arguments and results of external
1412 // region-bearing ops) are always pre-mapped to a concrete SSA value by
1413 // visitExternalOp, so materialize() short-circuits via the cache before
1414 // reaching here.
1415 emitError() << "cannot materialize opaque external value";
1416 return {};
1417 }
1418
1419private:
1420 /// Cache values we have already materialized to reuse them later. We start
1421 /// with an insertion point at the start of the block and cache the (updated)
1422 /// insertion point such that future materializations can also reuse previous
1423 /// materializations without running into dominance issues (or requiring
1424 /// additional checks to avoid them).
1425 DenseMap<ElaboratorValue, Value> materializedValues;
1426
1427 /// Cache the builder to continue insertions at their current insertion point
1428 /// for the reason stated above.
1429 OpBuilder builder;
1430
1431 /// The block this materializer was constructed at. Used as the hoist target
1432 /// for constants that would otherwise be sunk into nested sibling regions.
1433 Block *rootBlock;
1434
1435 SmallVector<Operation *> toDelete;
1436
1437 TestState &testState;
1438 SharedState &sharedState;
1439
1440 /// Keep track of the block arguments we had to add to this materializer's
1441 /// block for identity values and also remember which elaborator values are
1442 /// expected to be passed as arguments from outside.
1443 SmallVector<ElaboratorValue> &blockArgs;
1444 SmallVector<Type> blockArgTypes;
1445
1446 /// Identity values in this set are materialized by this materializer,
1447 /// otherwise they are added as block arguments and the block that wants to
1448 /// embed this sequence is expected to provide a value for it.
1449 DenseSet<IdentityValue *> identityValueRoot;
1450
1451 /// Helper to convert ElaboratorValues to attributes.
1452 ElaboratorValueToAttributeConverter attrConverter;
1453};
1454
1455//===----------------------------------------------------------------------===//
1456// Elaboration Visitor
1457//===----------------------------------------------------------------------===//
1458
1459/// Used to signal to the elaboration driver whether the operation should be
1460/// removed.
1461enum class DeletionKind {
1462 Keep,
1463 Delete,
1464 /// Stop iterating the current block; used when rtg.perform triggers a
1465 /// handler that elaborates the continuation inline.
1466 StopElaboration
1467};
1468
1469/// Interprets the IR to perform and lower the represented randomizations.
1470class Elaborator : public RTGOpVisitor<Elaborator, FailureOr<DeletionKind>> {
1471public:
1473 using RTGBase::visitOp;
1474
1475 Elaborator(SharedState &sharedState, TestState &testState,
1476 Materializer &materializer,
1477 ContextResourceAttrInterface currentContext = {})
1478 : sharedState(sharedState), testState(testState),
1479 materializer(materializer), currentContext(currentContext),
1480 attrConverter(sharedState.internalizer),
1481 elabValConverter(sharedState.ctxt) {}
1482
1483 template <typename ValueTy>
1484 inline ValueTy get(Value val) const {
1485 return std::get<ValueTy>(state.at(val));
1486 }
1487
1488 /// Print a nice error message for operations we don't support yet.
1489 FailureOr<DeletionKind> visitUnhandledOp(Operation *op) {
1490 return visitOpGeneric(op);
1491 }
1492
1493 FailureOr<DeletionKind> visitExternalOp(Operation *op) {
1494 if (op->getNumRegions() == 0)
1495 return visitOpGeneric(op);
1496
1497 // Elaborate all regions of unknown external ops. Any op appearing inside
1498 // an RTG test body with regions is expected to contain RTG constructs.
1499 //
1500 // The new op is a structural shell:
1501 // - Its operands are re-materialized from the elaborator state so that
1502 // references to RTG-elaborated values become the corresponding new
1503 // SSA values (and don't dangle after finalize() erases the old ops).
1504 // - Each region gets a fresh block whose arguments mirror the old
1505 // block's argument types/locations. Old block args are registered as
1506 // opaque IdentityValues mapped to the new block args, so RTG ops
1507 // inside the region can resolve them via state lookup.
1508 // - Each result is registered as an opaque IdentityValue mapped to the
1509 // corresponding new result, so downstream RTG ops consuming them
1510 // find them in state.
1511 auto *newOp = op->cloneWithoutRegions();
1512 materializer.getBuilder().insert(newOp);
1513
1514 // Re-map operands: substitute each operand with the SSA value
1515 // corresponding to its current ElaboratorValue. Materialization of
1516 // operands must happen *before* `newOp` so the defs dominate the use;
1517 // anchor the builder at `newOp` (= immediately before it) for the
1518 // duration of operand re-map, then advance past it.
1519 materializer.getBuilder().setInsertionPoint(newOp);
1520 for (auto &operand : newOp->getOpOperands()) {
1521 auto emitError = [&]() {
1522 auto diag = newOp->emitError();
1523 diag.attachNote(newOp->getLoc())
1524 << "while materializing operand#" << operand.getOperandNumber()
1525 << " of external region op";
1526 return diag;
1527 };
1528 auto elabVal = state.at(operand.get());
1529 Value val = materializer.materialize(elabVal, newOp->getLoc(), emitError);
1530 if (!val)
1531 return failure();
1532 operand.set(val);
1533 }
1534 materializer.getBuilder().setInsertionPointAfter(newOp);
1535
1536 // Register an old value as an opaque external value mapped to the
1537 // corresponding new SSA value. RTG ops that consume `oldVal` will find the
1538 // opaque value in `state` (it is treated as symbolic), and materializing it
1539 // yields `newVal` directly via the materializer's cache.
1540 auto mapOpaque = [&](Value oldVal, Value newVal) {
1541 auto *storage = sharedState.internalizer.create<OpaqueExternalStorage>(
1542 oldVal.getType(), oldVal.getLoc());
1543 state[oldVal] = storage;
1544 materializer.map(storage, newVal);
1545 };
1546
1547 for (auto [oldRegion, newRegion] :
1548 llvm::zip(op->getRegions(), newOp->getRegions())) {
1549 if (oldRegion.empty())
1550 continue;
1551
1552 // Give the new region a block whose arguments mirror the old block's
1553 // argument types/locations, and register the old args as opaque values
1554 // mapped to the new args so region-internal RTG ops can resolve them.
1555 Block &oldBlock = oldRegion.front();
1556 Block &newBlock = newRegion.emplaceBlock();
1557 for (auto oldArg : oldBlock.getArguments()) {
1558 Value newArg = newBlock.addArgument(oldArg.getType(), oldArg.getLoc());
1559 mapOpaque(oldArg, newArg);
1560 }
1561
1562 {
1563 OpBuilder::InsertionGuard guard(materializer.getBuilder());
1564 materializer.getBuilder().setInsertionPoint(&newBlock,
1565 newBlock.begin());
1566 SmallVector<ElaboratorValue> unused;
1567 // keepTerminator=true: the original terminator (rtg.yield, scf.yield,
1568 // etc.) is cloned into the new block, preserving the op's expected
1569 // terminator type.
1570 if (failed(elaborate(oldRegion, {},
1571 /*keepTerminator=*/true, unused)))
1572 return failure();
1573 }
1574 }
1575
1576 // Register each result as an opaque value mapped to the new op's result so
1577 // downstream RTG ops consuming them find them in `state`.
1578 for (auto [oldRes, newRes] :
1579 llvm::zip(op->getResults(), newOp->getResults()))
1580 mapOpaque(oldRes, newRes);
1581
1582 return DeletionKind::Delete;
1583 }
1584
1585 FailureOr<DeletionKind> visitOp(GetSequenceOp op) {
1586 SmallVector<ElaboratorValue> replacements;
1587 state[op.getResult()] =
1588 sharedState.internalizer.internalize<SequenceStorage>(
1589 op.getSequenceAttr().getAttr(), std::move(replacements));
1590 return DeletionKind::Delete;
1591 }
1592
1593 FailureOr<DeletionKind> visitOp(SubstituteSequenceOp op) {
1594 if (isSymbolic(state.at(op.getSequence())))
1595 return visitOpGeneric(op);
1596
1597 auto *seq = get<SequenceStorage *>(op.getSequence());
1598
1599 SmallVector<ElaboratorValue> replacements(seq->args);
1600 for (auto replacement : op.getReplacements())
1601 replacements.push_back(state.at(replacement));
1602
1603 state[op.getResult()] =
1604 sharedState.internalizer.internalize<SequenceStorage>(
1605 seq->familyName, std::move(replacements));
1606
1607 return DeletionKind::Delete;
1608 }
1609
1610 FailureOr<DeletionKind> visitOp(RandomizeSequenceOp op) {
1611 auto *seq = get<SequenceStorage *>(op.getSequence());
1612 auto *randomizedSeq =
1613 sharedState.internalizer.create<RandomizedSequenceStorage>(
1614 currentContext, seq, op.getLoc());
1615 materializer.registerIdentityValue(randomizedSeq);
1616 state[op.getResult()] =
1617 sharedState.internalizer.internalize<InterleavedSequenceStorage>(
1618 randomizedSeq);
1619 return DeletionKind::Delete;
1620 }
1621
1622 FailureOr<DeletionKind> visitOp(InterleaveSequencesOp op) {
1623 SmallVector<ElaboratorValue> sequences;
1624 for (auto seq : op.getSequences())
1625 sequences.push_back(state.at(seq));
1626
1627 state[op.getResult()] =
1628 sharedState.internalizer.internalize<InterleavedSequenceStorage>(
1629 std::move(sequences), op.getBatchSize());
1630 return DeletionKind::Delete;
1631 }
1632
1633 // NOLINTNEXTLINE(misc-no-recursion)
1634 LogicalResult isValidContext(ElaboratorValue value, Operation *op) const {
1635 if (std::holds_alternative<RandomizedSequenceStorage *>(value)) {
1636 auto *seq = std::get<RandomizedSequenceStorage *>(value);
1637 if (seq->context != currentContext) {
1638 auto err = op->emitError("attempting to place sequence derived from ")
1639 << seq->sequence->familyName.getValue() << " under context "
1640 << currentContext
1641 << ", but it was previously randomized for context ";
1642 if (seq->context)
1643 err << seq->context;
1644 else
1645 err << "'default'";
1646 return err;
1647 }
1648 return success();
1649 }
1650
1651 auto *interVal = std::get<InterleavedSequenceStorage *>(value);
1652 for (auto val : interVal->sequences)
1653 if (failed(isValidContext(val, op)))
1654 return failure();
1655 return success();
1656 }
1657
1658 FailureOr<DeletionKind> visitOp(EmbedSequenceOp op) {
1659 auto *seqVal = get<InterleavedSequenceStorage *>(op.getSequence());
1660 if (failed(isValidContext(seqVal, op)))
1661 return failure();
1662
1663 return DeletionKind::Keep;
1664 }
1665
1666 FailureOr<DeletionKind> visitOp(SetCreateOp op) {
1667 SetVector<ElaboratorValue> set;
1668 for (auto val : op.getElements())
1669 set.insert(state.at(val));
1670
1671 state[op.getSet()] = sharedState.internalizer.internalize<SetStorage>(
1672 std::move(set), op.getSet().getType());
1673 return DeletionKind::Delete;
1674 }
1675
1676 FailureOr<DeletionKind> visitOp(SetSelectRandomOp op) {
1677 auto set = get<SetStorage *>(op.getSet())->set;
1678
1679 if (set.empty())
1680 return op->emitError("cannot select from an empty set");
1681
1682 size_t selected = testState.rng.getUniformlyInRange(0, set.size() - 1);
1683 state[op.getResult()] = set[selected];
1684 return DeletionKind::Delete;
1685 }
1686
1687 FailureOr<DeletionKind> visitOp(SetDifferenceOp op) {
1688 auto original = get<SetStorage *>(op.getOriginal())->set;
1689 auto diff = get<SetStorage *>(op.getDiff())->set;
1690
1691 SetVector<ElaboratorValue> result(original);
1692 result.set_subtract(diff);
1693
1694 state[op.getResult()] = sharedState.internalizer.internalize<SetStorage>(
1695 std::move(result), op.getResult().getType());
1696 return DeletionKind::Delete;
1697 }
1698
1699 FailureOr<DeletionKind> visitOp(SetUnionOp op) {
1700 SetVector<ElaboratorValue> result;
1701 for (auto set : op.getSets())
1702 result.set_union(get<SetStorage *>(set)->set);
1703
1704 state[op.getResult()] = sharedState.internalizer.internalize<SetStorage>(
1705 std::move(result), op.getType());
1706 return DeletionKind::Delete;
1707 }
1708
1709 FailureOr<DeletionKind> visitOp(SetSizeOp op) {
1710 auto size = get<SetStorage *>(op.getSet())->set.size();
1711 state[op.getResult()] = size;
1712 return DeletionKind::Delete;
1713 }
1714
1715 // {a0,a1} x {b0,b1} x {c0,c1} -> {(a0), (a1)} -> {(a0,b0), (a0,b1), (a1,b0),
1716 // (a1,b1)} -> {(a0,b0,c0), (a0,b0,c1), (a0,b1,c0), (a0,b1,c1), (a1,b0,c0),
1717 // (a1,b0,c1), (a1,b1,c0), (a1,b1,c1)}
1718 FailureOr<DeletionKind> visitOp(SetCartesianProductOp op) {
1719 SetVector<ElaboratorValue> result;
1720 SmallVector<SmallVector<ElaboratorValue>> tuples;
1721 tuples.push_back({});
1722
1723 for (auto input : op.getInputs()) {
1724 auto &set = get<SetStorage *>(input)->set;
1725 if (set.empty()) {
1726 SetVector<ElaboratorValue> empty;
1727 state[op.getResult()] =
1728 sharedState.internalizer.internalize<SetStorage>(std::move(empty),
1729 op.getType());
1730 return DeletionKind::Delete;
1731 }
1732
1733 for (unsigned i = 0, e = tuples.size(); i < e; ++i) {
1734 for (auto setEl : set.getArrayRef().drop_back()) {
1735 tuples.push_back(tuples[i]);
1736 tuples.back().push_back(setEl);
1737 }
1738 tuples[i].push_back(set.back());
1739 }
1740 }
1741
1742 for (auto &tup : tuples)
1743 result.insert(
1744 sharedState.internalizer.internalize<TupleStorage>(std::move(tup)));
1745
1746 state[op.getResult()] = sharedState.internalizer.internalize<SetStorage>(
1747 std::move(result), op.getType());
1748 return DeletionKind::Delete;
1749 }
1750
1751 FailureOr<DeletionKind> visitOp(SetConvertToBagOp op) {
1752 auto set = get<SetStorage *>(op.getInput())->set;
1754 for (auto val : set)
1755 bag.insert({val, 1});
1756 state[op.getResult()] = sharedState.internalizer.internalize<BagStorage>(
1757 std::move(bag), op.getType());
1758 return DeletionKind::Delete;
1759 }
1760
1761 FailureOr<DeletionKind> visitOp(BagCreateOp op) {
1763 for (auto [val, multiple] :
1764 llvm::zip(op.getElements(), op.getMultiples())) {
1765 // If the multiple is not stored as an AttributeValue, the elaboration
1766 // must have already failed earlier (since we don't have
1767 // unevaluated/opaque values).
1768 bag[state.at(val)] += get<size_t>(multiple);
1769 }
1770
1771 state[op.getBag()] = sharedState.internalizer.internalize<BagStorage>(
1772 std::move(bag), op.getType());
1773 return DeletionKind::Delete;
1774 }
1775
1776 FailureOr<DeletionKind> visitOp(BagSelectRandomOp op) {
1777 auto bag = get<BagStorage *>(op.getBag())->bag;
1778
1779 if (bag.empty())
1780 return op->emitError("cannot select from an empty bag");
1781
1782 SmallVector<std::pair<ElaboratorValue, uint32_t>> prefixSum;
1783 prefixSum.reserve(bag.size());
1784 uint32_t accumulator = 0;
1785 for (auto [val, weight] : bag) {
1786 accumulator += weight;
1787 prefixSum.push_back({val, accumulator});
1788 }
1789
1790 auto idx = testState.rng.getUniformlyInRange(0, accumulator - 1);
1791 auto *iter = llvm::upper_bound(
1792 prefixSum, idx,
1793 [](uint32_t a, const std::pair<ElaboratorValue, uint32_t> &b) {
1794 return a < b.second;
1795 });
1796
1797 state[op.getResult()] = iter->first;
1798 return DeletionKind::Delete;
1799 }
1800
1801 FailureOr<DeletionKind> visitOp(BagDifferenceOp op) {
1802 auto original = get<BagStorage *>(op.getOriginal())->bag;
1803 auto diff = get<BagStorage *>(op.getDiff())->bag;
1804
1806 for (const auto &el : original) {
1807 if (!diff.contains(el.first)) {
1808 result.insert(el);
1809 continue;
1810 }
1811
1812 if (op.getInf())
1813 continue;
1814
1815 auto toDiff = diff.lookup(el.first);
1816 if (el.second <= toDiff)
1817 continue;
1818
1819 result.insert({el.first, el.second - toDiff});
1820 }
1821
1822 state[op.getResult()] = sharedState.internalizer.internalize<BagStorage>(
1823 std::move(result), op.getType());
1824 return DeletionKind::Delete;
1825 }
1826
1827 FailureOr<DeletionKind> visitOp(BagUnionOp op) {
1829 for (auto bag : op.getBags()) {
1830 auto val = get<BagStorage *>(bag)->bag;
1831 for (auto [el, multiple] : val)
1832 result[el] += multiple;
1833 }
1834
1835 state[op.getResult()] = sharedState.internalizer.internalize<BagStorage>(
1836 std::move(result), op.getType());
1837 return DeletionKind::Delete;
1838 }
1839
1840 FailureOr<DeletionKind> visitOp(BagUniqueSizeOp op) {
1841 auto size = get<BagStorage *>(op.getBag())->bag.size();
1842 state[op.getResult()] = size;
1843 return DeletionKind::Delete;
1844 }
1845
1846 FailureOr<DeletionKind> visitOp(BagConvertToSetOp op) {
1847 auto bag = get<BagStorage *>(op.getInput())->bag;
1848 SetVector<ElaboratorValue> set;
1849 for (auto [k, v] : bag)
1850 set.insert(k);
1851 state[op.getResult()] = sharedState.internalizer.internalize<SetStorage>(
1852 std::move(set), op.getType());
1853 return DeletionKind::Delete;
1854 }
1855
1856 FailureOr<DeletionKind> visitOp(VirtualRegisterOp op) {
1857 auto *val = sharedState.internalizer.create<VirtualRegisterStorage>(
1858 op.getAllowedRegsAttr(), op.getType(), op.getLoc());
1859 state[op.getResult()] = val;
1860 materializer.registerIdentityValue(val);
1861 return DeletionKind::Delete;
1862 }
1863
1864 FailureOr<DeletionKind> visitOp(ArrayCreateOp op) {
1865 SmallVector<ElaboratorValue> array;
1866 array.reserve(op.getElements().size());
1867 for (auto val : op.getElements())
1868 array.emplace_back(state.at(val));
1869
1870 state[op.getResult()] = sharedState.internalizer.internalize<ArrayStorage>(
1871 op.getResult().getType(), std::move(array));
1872 return DeletionKind::Delete;
1873 }
1874
1875 FailureOr<DeletionKind> visitOp(StringToASCIIArrayOp op) {
1876 auto opaque = state.at(op.getString());
1877 if (isSymbolic(opaque))
1878 return visitOpGeneric(op);
1879
1880 auto strAttr = dyn_cast<StringAttr>(std::get<TypedAttr>(opaque));
1881 if (!strAttr)
1882 return op->emitError("expected a string attribute");
1883
1884 auto i8Ty = IntegerType::get(op.getContext(), 8);
1885 SmallVector<ElaboratorValue> array;
1886 array.reserve(strAttr.getValue().size());
1887 for (unsigned char c : strAttr.getValue())
1888 array.push_back(ElaboratorValue(IntegerAttr::get(i8Ty, c)));
1889
1890 state[op.getResult()] = sharedState.internalizer.internalize<ArrayStorage>(
1891 op.getResult().getType(), std::move(array));
1892 return DeletionKind::Delete;
1893 }
1894
1895 FailureOr<DeletionKind> visitOp(ArrayExtractOp op) {
1896 auto array = get<ArrayStorage *>(op.getArray())->array;
1897 size_t idx = get<size_t>(op.getIndex());
1898
1899 if (array.size() <= idx)
1900 return op->emitError("invalid to access index ")
1901 << idx << " of an array with " << array.size() << " elements";
1902
1903 state[op.getResult()] = array[idx];
1904 return DeletionKind::Delete;
1905 }
1906
1907 FailureOr<DeletionKind> visitOp(ArrayInjectOp op) {
1908 auto arrayOpaque = state.at(op.getArray());
1909 auto idxOpaque = state.at(op.getIndex());
1910 if (isSymbolic(arrayOpaque) || isSymbolic(idxOpaque))
1911 return visitOpGeneric(op);
1912
1913 auto array = std::get<ArrayStorage *>(arrayOpaque)->array;
1914 size_t idx = std::get<size_t>(idxOpaque);
1915
1916 if (array.size() <= idx)
1917 return op->emitError("invalid to access index ")
1918 << idx << " of an array with " << array.size() << " elements";
1919
1920 array[idx] = state.at(op.getValue());
1921 state[op.getResult()] = sharedState.internalizer.internalize<ArrayStorage>(
1922 op.getResult().getType(), std::move(array));
1923 return DeletionKind::Delete;
1924 }
1925
1926 FailureOr<DeletionKind> visitOp(ArrayAppendOp op) {
1927 auto array = std::get<ArrayStorage *>(state.at(op.getArray()))->array;
1928 array.push_back(state.at(op.getElement()));
1929 state[op.getResult()] = sharedState.internalizer.internalize<ArrayStorage>(
1930 op.getResult().getType(), std::move(array));
1931 return DeletionKind::Delete;
1932 }
1933
1934 FailureOr<DeletionKind> visitOp(ArraySizeOp op) {
1935 auto array = get<ArrayStorage *>(op.getArray())->array;
1936 state[op.getResult()] = array.size();
1937 return DeletionKind::Delete;
1938 }
1939
1940 FailureOr<DeletionKind> visitOp(LabelUniqueDeclOp op) {
1941 auto *val = sharedState.internalizer.create<UniqueLabelStorage>(
1942 state.at(op.getNamePrefix()), op.getLoc());
1943 state[op.getLabel()] = val;
1944 materializer.registerIdentityValue(val);
1945 return DeletionKind::Delete;
1946 }
1947
1948 FailureOr<DeletionKind> visitOp(RandomScopeOp op) {
1949 auto getNestedRng = [&]() -> RngScope {
1950 if (op.getSeed())
1951 return RngScope(op.getSeed()->getZExtValue());
1952 return testState.rng.getNested();
1953 };
1954
1955 RngScope nestedRng = getNestedRng();
1956 std::swap(testState.rng, nestedRng);
1957
1958 // Elaborate the body region. The region has no arguments and yields values
1959 // that become the results of this operation.
1960 // We reuse the same elaborator for the nested region because we need access
1961 // to the elaborated values outside the nested region (since it is not
1962 // isolated from above) and we want to delete the random_scope operation
1963 // entirely, thus don't need a new materializer instance.
1964 SmallVector<ElaboratorValue> yieldedVals;
1965 if (failed(elaborate(op.getBodyRegion(), {}, /*keepTerminator=*/false,
1966 yieldedVals)))
1967 return failure();
1968
1969 // Restore the previous RNG scope.
1970 std::swap(testState.rng, nestedRng);
1971
1972 // Map the results of the random_scope to the yielded values.
1973 for (auto [res, out] : llvm::zip(op.getResults(), yieldedVals))
1974 state[res] = out;
1975
1976 return DeletionKind::Delete;
1977 }
1978
1979 FailureOr<DeletionKind> visitOp(RandomNumberInRangeOp op) {
1980 size_t lower = get<size_t>(op.getLowerBound());
1981 size_t upper = get<size_t>(op.getUpperBound());
1982 if (lower > upper)
1983 return op->emitError("cannot select a number from an empty range");
1984
1985 state[op.getResult()] =
1986 size_t(testState.rng.getUniformlyInRange(lower, upper));
1987 return DeletionKind::Delete;
1988 }
1989
1990 FailureOr<DeletionKind> visitOp(IntToImmediateOp op) {
1991 size_t input = get<size_t>(op.getInput());
1992 auto width = op.getType().getWidth();
1993 auto emitError = [&]() { return op->emitError(); };
1994 if (input > APInt::getAllOnes(width).getZExtValue())
1995 return emitError() << "cannot represent " << input << " with " << width
1996 << " bits";
1997
1998 state[op.getResult()] =
1999 IntegerAttr::get(IntegerType::get(op.getContext(), width), input);
2000 return DeletionKind::Delete;
2001 }
2002
2003 FailureOr<DeletionKind> visitOp(OnContextOp op) {
2004 ContextResourceAttrInterface from = currentContext,
2005 to = cast<ContextResourceAttrInterface>(
2006 get<TypedAttr>(op.getContext()));
2007 if (!currentContext)
2008 from = DefaultContextAttr::get(op->getContext(), to.getType());
2009
2010 auto emitError = [&]() {
2011 auto diag = op.emitError();
2012 diag.attachNote(op.getLoc())
2013 << "while materializing value for context switching for " << op;
2014 return diag;
2015 };
2016
2017 if (from == to) {
2018 Value seqVal = materializer.materialize(
2019 get<SequenceStorage *>(op.getSequence()), op.getLoc(), emitError);
2020 if (!seqVal)
2021 return failure();
2022
2023 Value randSeqVal =
2024 materializer.create<RandomizeSequenceOp>(op.getLoc(), seqVal);
2025 materializer.create<EmbedSequenceOp>(op.getLoc(), randSeqVal);
2026 return DeletionKind::Delete;
2027 }
2028
2029 // Switch to the desired context.
2030 // First, check if a context switch is registered that has the concrete
2031 // context as source and target.
2032 auto *iter = testState.contextSwitches.find({from, to});
2033
2034 // Try with 'any' context as target and the concrete context as source.
2035 if (iter == testState.contextSwitches.end())
2036 iter = testState.contextSwitches.find(
2037 {from, AnyContextAttr::get(op->getContext(), to.getType())});
2038
2039 // Try with 'any' context as source and the concrete context as target.
2040 if (iter == testState.contextSwitches.end())
2041 iter = testState.contextSwitches.find(
2042 {AnyContextAttr::get(op->getContext(), from.getType()), to});
2043
2044 // Try with 'any' context for both the source and the target.
2045 if (iter == testState.contextSwitches.end())
2046 iter = testState.contextSwitches.find(
2047 {AnyContextAttr::get(op->getContext(), from.getType()),
2048 AnyContextAttr::get(op->getContext(), to.getType())});
2049
2050 // Otherwise, fail with an error because we couldn't find a user
2051 // specification on how to switch between the requested contexts.
2052 // NOTE: we could think about supporting context switching via intermediate
2053 // context, i.e., treat it as a transitive relation.
2054 if (iter == testState.contextSwitches.end())
2055 return op->emitError("no context transition registered to switch from ")
2056 << from << " to " << to;
2057
2058 auto familyName = iter->second->familyName;
2059 SmallVector<ElaboratorValue> args{from, to,
2060 get<SequenceStorage *>(op.getSequence())};
2061 auto *seq = sharedState.internalizer.internalize<SequenceStorage>(
2062 familyName, std::move(args));
2063 auto *randSeq = sharedState.internalizer.create<RandomizedSequenceStorage>(
2064 to, seq, op.getLoc());
2065 materializer.registerIdentityValue(randSeq);
2066 Value seqVal = materializer.materialize(randSeq, op.getLoc(), emitError);
2067 if (!seqVal)
2068 return failure();
2069
2070 materializer.create<EmbedSequenceOp>(op.getLoc(), seqVal);
2071 return DeletionKind::Delete;
2072 }
2073
2074 FailureOr<DeletionKind> visitOp(ContextSwitchOp op) {
2075 testState.contextSwitches[{op.getFromAttr(), op.getToAttr()}] =
2076 get<SequenceStorage *>(op.getSequence());
2077 return DeletionKind::Delete;
2078 }
2079
2080 FailureOr<DeletionKind> visitOp(MemoryBlockDeclareOp op) {
2081 auto *val = sharedState.internalizer.create<MemoryBlockStorage>(
2082 op.getBaseAddress(), op.getEndAddress(), op.getType(), op.getLoc());
2083 state[op.getResult()] = val;
2084 materializer.registerIdentityValue(val);
2085 return DeletionKind::Delete;
2086 }
2087
2088 FailureOr<DeletionKind> visitOp(MemoryAllocOp op) {
2089 size_t size = get<size_t>(op.getSize());
2090 size_t alignment = get<size_t>(op.getAlignment());
2091 auto *memBlock = get<MemoryBlockStorage *>(op.getMemoryBlock());
2092 auto *val = sharedState.internalizer.create<MemoryStorage>(
2093 memBlock, size, alignment, op.getLoc());
2094 state[op.getResult()] = val;
2095 materializer.registerIdentityValue(val);
2096 return DeletionKind::Delete;
2097 }
2098
2099 FailureOr<DeletionKind> visitOp(MemorySizeOp op) {
2100 auto *memory = get<MemoryStorage *>(op.getMemory());
2101 state[op.getResult()] = memory->size;
2102 return DeletionKind::Delete;
2103 }
2104
2105 // Effect handler ops
2106 //===--------------------------------------------------------------------===//
2107
2108 FailureOr<DeletionKind> visitOp(EffectOp op) { return DeletionKind::Keep; }
2109
2110 FailureOr<DeletionKind> visitOp(WithHandlersOp op) {
2111 // Build the handler frame for this with_handlers scope.
2112 HandlerFrame frame;
2113 for (auto [effectAttr, handlerRegion] :
2114 llvm::zip(op.getEffects(), op.getHandlerRegions()))
2115 frame.handlers[cast<FlatSymbolRefAttr>(effectAttr).getAttr()] =
2116 &handlerRegion;
2117 handlerStack.push_back(std::move(frame));
2118
2119 SmallVector<ElaboratorValue> unused;
2120 if (failed(elaborate(op.getBody(), {}, /*keepTerminator=*/false, unused)))
2121 return failure();
2122
2123 // Pop only if not already consumed by a StopElaboration return.
2124 if (!handlerStack.empty())
2125 handlerStack.pop_back();
2126
2127 return DeletionKind::Delete;
2128 }
2129
2130 FailureOr<DeletionKind> visitOp(PerformOp op) {
2131 StringAttr effectName = op.getEffectAttr().getAttr();
2132
2133 // Search innermost-first for a handler for this effect.
2134 Region *handlerRegion = nullptr;
2135 for (int i = (int)handlerStack.size() - 1; i >= 0; --i) {
2136 auto it = handlerStack[i].handlers.find(effectName);
2137 if (it != handlerStack[i].handlers.end()) {
2138 handlerRegion = it->second;
2139 break;
2140 }
2141 }
2142 if (!handlerRegion)
2143 return op->emitError("no handler for effect @") << effectName.getValue();
2144
2145 // Collect remaining ops after this perform (for the continuation).
2146 SmallVector<Operation *> remaining;
2147 auto *block = op->getBlock();
2148 for (auto it = std::next(op->getIterator());
2149 it != block->end() && !it->hasTrait<OpTrait::IsTerminator>(); ++it)
2150 remaining.push_back(&*it);
2151
2152 // Determine the resume type (null Value → unit-result effect).
2153 Value performResultSSAVal =
2154 op.getNumResults() > 0 ? Value(op.getResult()) : Value();
2155 Type resumeType = performResultSSAVal ? performResultSSAVal.getType()
2156 : NoneType::get(op.getContext());
2157
2158 auto *cont = sharedState.internalizer.create<ContinuationStorage>(
2159 std::move(remaining), performResultSSAVal,
2160 SmallVector<HandlerFrame>(handlerStack), resumeType, op.getLoc());
2161
2162 // Build handler arguments: effect operands followed by the continuation.
2163 SmallVector<ElaboratorValue> handlerArgs;
2164 for (auto operand : op.getOperands())
2165 handlerArgs.push_back(state.at(operand));
2166 handlerArgs.push_back(cont);
2167
2168 // Pop the frame that owns this handler before entering the handler body
2169 // so the handler itself doesn't see its own effect.
2170 HandlerFrame savedFrame = handlerStack.back();
2171 handlerStack.pop_back();
2172
2173 SmallVector<ElaboratorValue> unused;
2174 if (failed(elaborate(*handlerRegion, handlerArgs,
2175 /*keepTerminator=*/false, unused)))
2176 return failure();
2177
2178 handlerStack.push_back(std::move(savedFrame));
2179
2180 return DeletionKind::StopElaboration;
2181 }
2182
2183 FailureOr<DeletionKind> visitOp(ResumeOp op) {
2184 auto *cont =
2185 std::get<ContinuationStorage *>(state.at(op.getContinuation()));
2186
2187 // If the perform had a result, map it to the resume value.
2188 if (cont->performResult && op.getValue())
2189 state[cont->performResult] = state.at(op.getValue());
2190
2191 // Temporarily restore the handler stack from when perform was triggered.
2192 auto savedStack = std::move(handlerStack);
2193 handlerStack = cont->capturedHandlerStack;
2194
2195 for (auto *contOp : cont->remainingOps) {
2196 auto result = dispatchOpVisitor(contOp);
2197 if (failed(result)) {
2198 handlerStack = std::move(savedStack);
2199 return failure();
2200 }
2201 if (*result == DeletionKind::StopElaboration)
2202 break;
2203 if (*result == DeletionKind::Keep)
2204 if (failed(materializer.materialize(contOp, state))) {
2205 handlerStack = std::move(savedStack);
2206 return failure();
2207 }
2208 LLVM_DEBUG({
2209 llvm::dbgs() << "Elaborated continuation op " << *contOp << " to\n[";
2210 llvm::interleaveComma(contOp->getResults(), llvm::dbgs(),
2211 [&](auto res) {
2212 if (state.contains(res))
2213 llvm::dbgs() << state.at(res);
2214 else
2215 llvm::dbgs() << "unknown";
2216 });
2217 llvm::dbgs() << "]\n\n";
2218 });
2219 }
2220
2221 handlerStack = std::move(savedStack);
2222 return DeletionKind::Delete;
2223 }
2224
2225 //===--------------------------------------------------------------------===//
2226
2227 FailureOr<DeletionKind> visitOp(TupleCreateOp op) {
2228 SmallVector<ElaboratorValue> values;
2229 values.reserve(op.getElements().size());
2230 for (auto el : op.getElements())
2231 values.push_back(state.at(el));
2232
2233 state[op.getResult()] =
2234 sharedState.internalizer.internalize<TupleStorage>(std::move(values));
2235 return DeletionKind::Delete;
2236 }
2237
2238 FailureOr<DeletionKind> visitOp(TupleExtractOp op) {
2239 auto *tuple = get<TupleStorage *>(op.getTuple());
2240 state[op.getResult()] = tuple->values[op.getIndex().getZExtValue()];
2241 return DeletionKind::Delete;
2242 }
2243
2244 FailureOr<DeletionKind> visitOp(scf::IfOp op) {
2245 bool cond = get<bool>(op.getCondition());
2246 auto &toElaborate = cond ? op.getThenRegion() : op.getElseRegion();
2247 if (toElaborate.empty())
2248 return DeletionKind::Delete;
2249
2250 // Just reuse this elaborator for the nested region because we need access
2251 // to the elaborated values outside the nested region (since it is not
2252 // isolated from above) and we want to materialize the region inline, thus
2253 // don't need a new materializer instance.
2254 SmallVector<ElaboratorValue> yieldedVals;
2255 if (failed(
2256 elaborate(toElaborate, {}, /*keepTerminator=*/false, yieldedVals)))
2257 return failure();
2258
2259 // Map the results of the 'scf.if' to the yielded values.
2260 for (auto [res, out] : llvm::zip(op.getResults(), yieldedVals))
2261 state[res] = out;
2262
2263 return DeletionKind::Delete;
2264 }
2265
2266 FailureOr<DeletionKind> visitOp(scf::ForOp op) {
2267 if (!(std::holds_alternative<size_t>(state.at(op.getLowerBound())) &&
2268 std::holds_alternative<size_t>(state.at(op.getStep())) &&
2269 std::holds_alternative<size_t>(state.at(op.getUpperBound()))))
2270 return op->emitOpError("can only elaborate index type iterator");
2271
2272 auto lowerBound = get<size_t>(op.getLowerBound());
2273 auto step = get<size_t>(op.getStep());
2274 auto upperBound = get<size_t>(op.getUpperBound());
2275
2276 // Prepare for first iteration by assigning the nested regions block
2277 // arguments. We can just reuse this elaborator because we need access to
2278 // values elaborated in the parent region anyway and materialize everything
2279 // inline (i.e., don't need a new materializer).
2280 state[op.getInductionVar()] = lowerBound;
2281 for (auto [iterArg, initArg] :
2282 llvm::zip(op.getRegionIterArgs(), op.getInitArgs()))
2283 state[iterArg] = state.at(initArg);
2284
2285 // This loop performs the actual 'scf.for' loop iterations.
2286 SmallVector<ElaboratorValue> yieldedVals;
2287 for (size_t i = lowerBound; i < upperBound; i += step) {
2288 yieldedVals.clear();
2289 if (failed(elaborate(op.getBodyRegion(), {}, /*keepTerminator=*/false,
2290 yieldedVals)))
2291 return failure();
2292
2293 // Prepare for the next iteration by updating the mapping of the nested
2294 // regions block arguments
2295 state[op.getInductionVar()] = i + step;
2296 for (auto [iterArg, prevIterArg] :
2297 llvm::zip(op.getRegionIterArgs(), yieldedVals))
2298 state[iterArg] = prevIterArg;
2299 }
2300
2301 // Transfer the previously yielded values to the for loop result values.
2302 for (auto [res, iterArg] :
2303 llvm::zip(op->getResults(), op.getRegionIterArgs()))
2304 state[res] = state.at(iterArg);
2305
2306 return DeletionKind::Delete;
2307 }
2308
2309 FailureOr<DeletionKind> visitOp(arith::AddIOp op) {
2310 if (!isa<IndexType>(op.getType()))
2311 return visitOpGeneric(op);
2312
2313 size_t lhs = get<size_t>(op.getLhs());
2314 size_t rhs = get<size_t>(op.getRhs());
2315 state[op.getResult()] = lhs + rhs;
2316 return DeletionKind::Delete;
2317 }
2318
2319 FailureOr<DeletionKind> visitOp(arith::AndIOp op) {
2320 if (!op.getType().isSignlessInteger(1))
2321 return visitOpGeneric(op);
2322
2323 bool lhs = get<bool>(op.getLhs());
2324 bool rhs = get<bool>(op.getRhs());
2325 state[op.getResult()] = lhs && rhs;
2326 return DeletionKind::Delete;
2327 }
2328
2329 FailureOr<DeletionKind> visitOp(arith::XOrIOp op) {
2330 if (!op.getType().isSignlessInteger(1))
2331 return visitOpGeneric(op);
2332
2333 bool lhs = get<bool>(op.getLhs());
2334 bool rhs = get<bool>(op.getRhs());
2335 state[op.getResult()] = lhs != rhs;
2336 return DeletionKind::Delete;
2337 }
2338
2339 FailureOr<DeletionKind> visitOp(arith::OrIOp op) {
2340 if (!op.getType().isSignlessInteger(1))
2341 return visitOpGeneric(op);
2342
2343 bool lhs = get<bool>(op.getLhs());
2344 bool rhs = get<bool>(op.getRhs());
2345 state[op.getResult()] = lhs || rhs;
2346 return DeletionKind::Delete;
2347 }
2348
2349 FailureOr<DeletionKind> visitOp(arith::SelectOp op) {
2350 auto condOpaque = state.at(op.getCondition());
2351 if (isSymbolic(condOpaque))
2352 return visitOpGeneric(op);
2353
2354 bool cond = std::get<bool>(condOpaque);
2355 auto trueVal = state.at(op.getTrueValue());
2356 auto falseVal = state.at(op.getFalseValue());
2357 state[op.getResult()] = cond ? trueVal : falseVal;
2358 return DeletionKind::Delete;
2359 }
2360
2361 FailureOr<DeletionKind> visitOp(index::AddOp op) {
2362 size_t lhs = get<size_t>(op.getLhs());
2363 size_t rhs = get<size_t>(op.getRhs());
2364 state[op.getResult()] = lhs + rhs;
2365 return DeletionKind::Delete;
2366 }
2367
2368 FailureOr<DeletionKind> visitOp(index::SubOp op) {
2369 size_t lhs = get<size_t>(op.getLhs());
2370 size_t rhs = get<size_t>(op.getRhs());
2371 state[op.getResult()] = lhs - rhs;
2372 return DeletionKind::Delete;
2373 }
2374
2375 FailureOr<DeletionKind> visitOp(index::MulOp op) {
2376 size_t lhs = get<size_t>(op.getLhs());
2377 size_t rhs = get<size_t>(op.getRhs());
2378 state[op.getResult()] = lhs * rhs;
2379 return DeletionKind::Delete;
2380 }
2381
2382 FailureOr<DeletionKind> visitOp(index::DivUOp op) {
2383 size_t lhs = get<size_t>(op.getLhs());
2384 size_t rhs = get<size_t>(op.getRhs());
2385
2386 if (rhs == 0)
2387 return op->emitOpError("attempted division by zero");
2388
2389 state[op.getResult()] = lhs / rhs;
2390 return DeletionKind::Delete;
2391 }
2392
2393 FailureOr<DeletionKind> visitOp(index::CeilDivUOp op) {
2394 size_t lhs = get<size_t>(op.getLhs());
2395 size_t rhs = get<size_t>(op.getRhs());
2396
2397 if (rhs == 0)
2398 return op->emitOpError("attempted division by zero");
2399
2400 if (lhs == 0)
2401 state[op.getResult()] = (lhs + rhs - 1) / rhs;
2402 else
2403 state[op.getResult()] = 1 + ((lhs - 1) / rhs);
2404
2405 return DeletionKind::Delete;
2406 }
2407
2408 FailureOr<DeletionKind> visitOp(index::RemUOp op) {
2409 size_t lhs = get<size_t>(op.getLhs());
2410 size_t rhs = get<size_t>(op.getRhs());
2411
2412 if (rhs == 0)
2413 return op->emitOpError("attempted division by zero");
2414
2415 state[op.getResult()] = lhs % rhs;
2416 return DeletionKind::Delete;
2417 }
2418
2419 FailureOr<DeletionKind> visitOp(index::AndOp op) {
2420 size_t lhs = get<size_t>(op.getLhs());
2421 size_t rhs = get<size_t>(op.getRhs());
2422 state[op.getResult()] = lhs & rhs;
2423 return DeletionKind::Delete;
2424 }
2425
2426 FailureOr<DeletionKind> visitOp(index::OrOp op) {
2427 size_t lhs = get<size_t>(op.getLhs());
2428 size_t rhs = get<size_t>(op.getRhs());
2429 state[op.getResult()] = lhs | rhs;
2430 return DeletionKind::Delete;
2431 }
2432
2433 FailureOr<DeletionKind> visitOp(index::XOrOp op) {
2434 size_t lhs = get<size_t>(op.getLhs());
2435 size_t rhs = get<size_t>(op.getRhs());
2436 state[op.getResult()] = lhs ^ rhs;
2437 return DeletionKind::Delete;
2438 }
2439
2440 FailureOr<DeletionKind> visitOp(index::ShlOp op) {
2441 size_t lhs = get<size_t>(op.getLhs());
2442 size_t rhs = get<size_t>(op.getRhs());
2443 state[op.getResult()] = lhs << rhs;
2444 return DeletionKind::Delete;
2445 }
2446
2447 FailureOr<DeletionKind> visitOp(index::ShrUOp op) {
2448 size_t lhs = get<size_t>(op.getLhs());
2449 size_t rhs = get<size_t>(op.getRhs());
2450 state[op.getResult()] = lhs >> rhs;
2451 return DeletionKind::Delete;
2452 }
2453
2454 FailureOr<DeletionKind> visitOp(index::MaxUOp op) {
2455 size_t lhs = get<size_t>(op.getLhs());
2456 size_t rhs = get<size_t>(op.getRhs());
2457 state[op.getResult()] = std::max(lhs, rhs);
2458 return DeletionKind::Delete;
2459 }
2460
2461 FailureOr<DeletionKind> visitOp(index::MinUOp op) {
2462 size_t lhs = get<size_t>(op.getLhs());
2463 size_t rhs = get<size_t>(op.getRhs());
2464 state[op.getResult()] = std::min(lhs, rhs);
2465 return DeletionKind::Delete;
2466 }
2467
2468 FailureOr<DeletionKind> visitOp(index::CmpOp op) {
2469 size_t lhs = get<size_t>(op.getLhs());
2470 size_t rhs = get<size_t>(op.getRhs());
2471 bool result;
2472 switch (op.getPred()) {
2473 case index::IndexCmpPredicate::EQ:
2474 result = lhs == rhs;
2475 break;
2476 case index::IndexCmpPredicate::NE:
2477 result = lhs != rhs;
2478 break;
2479 case index::IndexCmpPredicate::ULT:
2480 result = lhs < rhs;
2481 break;
2482 case index::IndexCmpPredicate::ULE:
2483 result = lhs <= rhs;
2484 break;
2485 case index::IndexCmpPredicate::UGT:
2486 result = lhs > rhs;
2487 break;
2488 case index::IndexCmpPredicate::UGE:
2489 result = lhs >= rhs;
2490 break;
2491 default:
2492 return op->emitOpError("elaboration not supported");
2493 }
2494 state[op.getResult()] = result;
2495 return DeletionKind::Delete;
2496 }
2497
2498 bool isSymbolic(ElaboratorValue val) {
2499 return std::holds_alternative<SymbolicComputationWithIdentityValue *>(
2500 val) ||
2501 std::holds_alternative<SymbolicComputationWithIdentityStorage *>(
2502 val) ||
2503 std::holds_alternative<SymbolicComputationStorage *>(val) ||
2504 std::holds_alternative<OpaqueExternalStorage *>(val);
2505 }
2506
2507 bool isSymbolic(Operation *op) {
2508 return llvm::any_of(op->getOperands(), [&](auto operand) {
2509 auto val = state.at(operand);
2510 return isSymbolic(val);
2511 });
2512 }
2513
2514 /// If all operands are constants, try to fold the operation and register its
2515 /// result values with the folded results in the elaborator state.
2516 /// Returns 'false' if operation has to be handled symbolically.
2517 bool attemptConcreteCase(Operation *op) {
2518 if (op->getNumResults() == 0)
2519 return false;
2520
2521 SmallVector<Attribute> operands;
2522 for (auto operand : op->getOperands()) {
2523 auto evalValue = state[operand];
2524 auto attr = elabValConverter.convert(evalValue);
2525 operands.push_back(attr);
2526 }
2527
2528 SmallVector<OpFoldResult> results;
2529 if (failed(op->fold(operands, results)))
2530 return false;
2531
2532 if (results.size() != op->getNumResults())
2533 return false;
2534
2535 for (auto [res, val] : llvm::zip(results, op->getResults())) {
2536 auto attr = llvm::dyn_cast_or_null<TypedAttr>(res.dyn_cast<Attribute>());
2537 if (!attr)
2538 return false;
2539
2540 if (attr.getType() != val.getType())
2541 return false;
2542
2543 // Try to convert the attribute to an ElaboratorValue
2544 auto converted = attrConverter.convert(attr);
2545 if (succeeded(converted)) {
2546 state[val] = *converted;
2547 continue;
2548 }
2549 return false;
2550 }
2551
2552 return true;
2553 }
2554
2555 FailureOr<DeletionKind> visitOpGeneric(Operation *op) {
2556 if (op->getNumResults() == 0)
2557 return DeletionKind::Keep;
2558
2559 if (attemptConcreteCase(op))
2560 return DeletionKind::Delete;
2561
2562 if (mlir::isMemoryEffectFree(op)) {
2563 if (op->getNumResults() != 1)
2564 return op->emitOpError(
2565 "symbolic elaboration of memory-effect-free operations with "
2566 "multiple results not supported");
2567
2568 state[op->getResult(0)] =
2569 sharedState.internalizer.internalize<SymbolicComputationStorage>(
2570 state, op);
2571 return DeletionKind::Delete;
2572 }
2573
2574 // We assume that reordering operations with only allocate effects is
2575 // allowed.
2576 // FIXME: this is not how the MLIR MemoryEffects interface intends it.
2577 // We should create our own interface/trait for that use-case.
2578 // Or modify the elaboration pass to keep track of the ordering of such
2579 // instructions and materialize all operations that are not already
2580 // materialized but have to happen before the current alloc operation to be
2581 // materialized.
2582 bool onlyAlloc = mlir::hasSingleEffect<mlir::MemoryEffects::Allocate>(op);
2583 onlyAlloc |= isa<ValidateOp>(op);
2584
2585 auto *validationVal =
2586 sharedState.internalizer.create<SymbolicComputationWithIdentityStorage>(
2587 state, op);
2588 materializer.registerIdentityValue(validationVal);
2589 state[op->getResult(0)] = validationVal;
2590
2591 for (auto [i, res] : llvm::enumerate(op->getResults())) {
2592 if (i == 0)
2593 continue;
2594 auto *val =
2595 sharedState.internalizer.create<SymbolicComputationWithIdentityValue>(
2596 res.getType(), validationVal, i);
2597 state[res] = val;
2598 materializer.registerIdentityValue(val);
2599 }
2600 return onlyAlloc ? DeletionKind::Delete : DeletionKind::Keep;
2601 }
2602
2603 bool supportsSymbolicValuesNonGenerically(Operation *op) {
2604 return isa<SubstituteSequenceOp, ArrayCreateOp, ArrayInjectOp,
2605 TupleCreateOp, arith::SelectOp>(op);
2606 }
2607
2608 FailureOr<DeletionKind> dispatchOpVisitor(Operation *op) {
2609 if (isSymbolic(op) && !supportsSymbolicValuesNonGenerically(op))
2610 return visitOpGeneric(op);
2611
2612 return TypeSwitch<Operation *, FailureOr<DeletionKind>>(op)
2613 .Case<
2614 // Arith ops
2615 arith::AddIOp, arith::XOrIOp, arith::AndIOp, arith::OrIOp,
2616 arith::SelectOp,
2617 // Index ops
2618 index::AddOp, index::SubOp, index::MulOp, index::DivUOp,
2619 index::CeilDivUOp, index::RemUOp, index::AndOp, index::OrOp,
2620 index::XOrOp, index::ShlOp, index::ShrUOp, index::MaxUOp,
2621 index::MinUOp, index::CmpOp,
2622 // SCF ops
2623 scf::IfOp, scf::ForOp>([&](auto op) { return visitOp(op); })
2624 .Default([&](Operation *op) { return RTGBase::dispatchOpVisitor(op); });
2625 }
2626
2627 // NOLINTNEXTLINE(misc-no-recursion)
2628 LogicalResult elaborate(Region &region,
2629 ArrayRef<ElaboratorValue> regionArguments,
2630 bool keepTerminator,
2631 SmallVector<ElaboratorValue> &terminatorOperands) {
2632 if (region.getBlocks().size() > 1)
2633 return region.getParentOp()->emitOpError(
2634 "regions with more than one block are not supported");
2635
2636 // Save any prior bindings for this region's block arguments so that a
2637 // recursive re-entry of the same region (e.g. nested invocation of the
2638 // same handler region during a multi-shot resume whose continuation body
2639 // re-performs the handled effect) does not clobber the outer frame's
2640 // bindings of those args. Without this, the outer handler's continuation
2641 // SSA value is rebound to the inner frame's continuation, and the outer
2642 // handler's subsequent `rtg.resume %k, ...` resumes the inner frame.
2643 SmallVector<std::pair<Value, std::optional<ElaboratorValue>>> savedArgs;
2644 savedArgs.reserve(region.getNumArguments());
2645 for (auto arg : region.getArguments()) {
2646 auto it = state.find(arg);
2647 if (it != state.end())
2648 savedArgs.emplace_back(arg, it->second);
2649 else
2650 savedArgs.emplace_back(arg, std::nullopt);
2651 }
2652 llvm::scope_exit restoreArgs([&] {
2653 for (auto &[arg, prev] : savedArgs) {
2654 if (prev.has_value())
2655 state[arg] = *prev;
2656 else
2657 state.erase(arg);
2658 }
2659 });
2660
2661 for (auto [arg, elabArg] :
2662 llvm::zip(region.getArguments(), regionArguments))
2663 state[arg] = elabArg;
2664
2665 Block *block = &region.front();
2666 auto iter = keepTerminator ? *block : block->without_terminator();
2667 for (auto &op : iter) {
2668 auto result = dispatchOpVisitor(&op);
2669 if (failed(result))
2670 return failure();
2671
2672 // A perform op elaborated the continuation inline; remaining ops in this
2673 // block will be deleted by finalizer.
2674 if (*result == DeletionKind::StopElaboration)
2675 return success();
2676
2677 if (*result == DeletionKind::Keep)
2678 if (failed(materializer.materialize(&op, state)))
2679 return failure();
2680
2681 LLVM_DEBUG({
2682 llvm::dbgs() << "Elaborated " << op << " to\n[";
2683
2684 llvm::interleaveComma(op.getResults(), llvm::dbgs(), [&](auto res) {
2685 if (state.contains(res))
2686 llvm::dbgs() << state.at(res);
2687 else
2688 llvm::dbgs() << "unknown";
2689 });
2690
2691 llvm::dbgs() << "]\n\n";
2692 });
2693 }
2694
2695 if (!block->empty() && block->back().hasTrait<OpTrait::IsTerminator>()) {
2696 auto *terminator = block->getTerminator();
2697 for (auto val : terminator->getOperands())
2698 terminatorOperands.push_back(state.at(val));
2699
2700 if (!keepTerminator && materializer.isInPlace(terminator))
2701 terminator->erase();
2702 }
2703
2704 return success();
2705 }
2706
2707private:
2708 // State to be shared between all elaborator instances.
2709 SharedState &sharedState;
2710
2711 // State to a specific RTG test and the sequences placed within it.
2712 TestState &testState;
2713
2714 // Allows us to materialize ElaboratorValues to the IR operations necessary to
2715 // obtain an SSA value representing that elaborated value.
2716 Materializer &materializer;
2717
2718 // A map from SSA values to a pointer of an interned elaborator value.
2719 DenseMap<Value, ElaboratorValue> state;
2720
2721 // The current context we are elaborating under.
2722 ContextResourceAttrInterface currentContext;
2723
2724 // Allows us to convert attributes to ElaboratorValues.
2725 AttributeToElaboratorValueConverter attrConverter;
2726
2727 // Allows us to convert ElaboratorValues to attributes.
2728 ElaboratorValueToAttributeConverter elabValConverter;
2729
2730 // Stack of handler frames for rtg.handle scopes, innermost last.
2731 SmallVector<HandlerFrame> handlerStack;
2732};
2733} // namespace
2734
2735SequenceOp
2736Materializer::elaborateSequence(const RandomizedSequenceStorage *seq,
2737 SmallVector<ElaboratorValue> &elabArgs) {
2738 auto familyOp =
2739 sharedState.table.lookup<SequenceOp>(seq->sequence->familyName);
2740 // TODO: don't clone if this is the only remaining reference to this
2741 // sequence
2742 OpBuilder builder(familyOp);
2743 auto seqOp = builder.cloneWithoutRegions(familyOp);
2744 auto name = sharedState.names.newName(seq->sequence->familyName.getValue());
2745 seqOp.setSymName(name);
2746 seqOp.getBodyRegion().emplaceBlock();
2747 sharedState.table.insert(seqOp);
2748 assert(seqOp.getSymName() == name && "should not have been renamed");
2749
2750 LLVM_DEBUG(llvm::dbgs() << "\n=== Elaborating sequence family @"
2751 << familyOp.getSymName() << " into @"
2752 << seqOp.getSymName() << " under context "
2753 << seq->context << "\n\n");
2754
2755 Materializer materializer(OpBuilder::atBlockBegin(seqOp.getBody()), testState,
2756 sharedState, elabArgs);
2757 Elaborator elaborator(sharedState, testState, materializer, seq->context);
2758 SmallVector<ElaboratorValue> yieldedVals;
2759 if (failed(elaborator.elaborate(familyOp.getBodyRegion(), seq->sequence->args,
2760 /*keepTerminator=*/false, yieldedVals)))
2761 return {};
2762
2763 seqOp.setSequenceType(
2764 SequenceType::get(builder.getContext(), materializer.getBlockArgTypes()));
2765 materializer.finalize();
2766
2767 return seqOp;
2768}
2769
2770//===----------------------------------------------------------------------===//
2771// Elaborator Pass
2772//===----------------------------------------------------------------------===//
2773
2774namespace {
2775struct ElaborationPass
2776 : public rtg::impl::ElaborationPassBase<ElaborationPass> {
2777 using Base::Base;
2778
2779 void runOnOperation() override;
2780 void matchTestsAgainstTargets(SymbolTable &table);
2781 LogicalResult elaborateModule(ModuleOp moduleOp, SymbolTable &table);
2782};
2783} // namespace
2784
2785void ElaborationPass::runOnOperation() {
2786 auto moduleOp = getOperation();
2787 SymbolTable table(moduleOp);
2788
2789 matchTestsAgainstTargets(table);
2790
2791 if (failed(elaborateModule(moduleOp, table)))
2792 return signalPassFailure();
2793}
2794
2795void ElaborationPass::matchTestsAgainstTargets(SymbolTable &table) {
2796 auto moduleOp = getOperation();
2797
2798 for (auto test : llvm::make_early_inc_range(moduleOp.getOps<TestOp>())) {
2799 if (test.getTargetAttr())
2800 continue;
2801
2802 bool matched = false;
2803
2804 for (auto target : moduleOp.getOps<TargetOp>()) {
2805 // Check if the target type is a subtype of the test's target type
2806 // This means that for each entry in the test's target type, there must be
2807 // a corresponding entry with the same name and type in the target's type
2808 bool isSubtype = true;
2809 auto testEntries = test.getTargetType().getEntries();
2810 auto targetEntries = target.getTarget().getEntries();
2811
2812 // Check if target is a subtype of test requirements
2813 // Since entries are sorted by name, we can do this in a single pass
2814 size_t targetIdx = 0;
2815 for (auto testEntry : testEntries) {
2816 // Find the matching entry in target entries.
2817 while (targetIdx < targetEntries.size() &&
2818 targetEntries[targetIdx].name.getValue() <
2819 testEntry.name.getValue())
2820 targetIdx++;
2821
2822 // Check if we found a matching entry with the same name and type
2823 if (targetIdx >= targetEntries.size() ||
2824 targetEntries[targetIdx].name != testEntry.name ||
2825 targetEntries[targetIdx].type != testEntry.type) {
2826 isSubtype = false;
2827 break;
2828 }
2829 }
2830
2831 if (!isSubtype)
2832 continue;
2833
2834 IRRewriter rewriter(test);
2835 // Create a new test for the matched target
2836 auto newTest = cast<TestOp>(test->clone());
2837 newTest.setSymName(test.getSymName().str() + "_" +
2838 target.getSymName().str());
2839
2840 // Set the target symbol specifying that this test is only suitable for
2841 // that target.
2842 newTest.setTargetAttr(target.getSymNameAttr());
2843
2844 table.insert(newTest, rewriter.getInsertionPoint());
2845 matched = true;
2846 }
2847
2848 if (matched || deleteUnmatchedTests)
2849 test->erase();
2850 }
2851}
2852
2853static bool onlyLegalToMaterializeInTarget(Type type) {
2854 return isa<MemoryBlockType, ContextResourceTypeInterface>(type);
2855}
2856
2857LogicalResult ElaborationPass::elaborateModule(ModuleOp moduleOp,
2858 SymbolTable &table) {
2859 SharedState state(moduleOp.getContext(), table);
2860
2861 // Update the name cache
2862 state.names.add(moduleOp);
2863
2864 struct TargetElabResult {
2865 TargetElabResult(DictType targetType, uint32_t seed)
2866 : targetType(targetType), testState(seed) {}
2867
2868 DictType targetType;
2869 SmallVector<ElaboratorValue> yields;
2870 TestState testState;
2871 };
2872
2873 // Map to store elaborated targets
2874 DenseMap<StringAttr, TargetElabResult> targetMap;
2875 for (auto targetOp : moduleOp.getOps<TargetOp>()) {
2876 LLVM_DEBUG(llvm::dbgs() << "=== Elaborating target @"
2877 << targetOp.getSymName() << "\n\n");
2878
2879 auto [it, inserted] = targetMap.try_emplace(targetOp.getSymNameAttr(),
2880 targetOp.getTarget(), seed);
2881 auto &result = it->second;
2882
2883 SmallVector<ElaboratorValue> blockArgs;
2884 Materializer targetMaterializer(OpBuilder::atBlockBegin(targetOp.getBody()),
2885 result.testState, state, blockArgs);
2886 Elaborator targetElaborator(state, result.testState, targetMaterializer);
2887
2888 // Elaborate the target
2889 if (failed(targetElaborator.elaborate(targetOp.getBodyRegion(), {},
2890 /*keepTerminator=*/true,
2891 result.yields)))
2892 return failure();
2893
2894 targetMaterializer.finalize();
2895 }
2896
2897 // Initialize the worklist with the test ops since they cannot be placed by
2898 // other ops.
2899 for (auto testOp : moduleOp.getOps<TestOp>()) {
2900 // Skip tests without a target attribute - these couldn't be matched
2901 // against any target but can be useful to keep around for reporting
2902 // purposes.
2903 if (!testOp.getTargetAttr())
2904 continue;
2905
2906 LLVM_DEBUG(llvm::dbgs()
2907 << "\n=== Elaborating test @" << testOp.getTemplateName()
2908 << " for target @" << *testOp.getTarget() << "\n\n");
2909
2910 // Get the target for this test
2911 auto &targetResult = targetMap.at(testOp.getTargetAttr());
2912 TestState testState(seed);
2913 testState.contextSwitches = targetResult.testState.contextSwitches;
2914 testState.name = testOp.getSymNameAttr();
2915
2916 SmallVector<ElaboratorValue> filteredYields;
2917 unsigned i = 0;
2918 for (auto [entry, yield] :
2919 llvm::zip(targetResult.targetType.getEntries(), targetResult.yields)) {
2920 if (i >= testOp.getTargetType().getEntries().size())
2921 break;
2922
2923 if (entry.name == testOp.getTargetType().getEntries()[i].name) {
2924 filteredYields.push_back(yield);
2925 ++i;
2926 }
2927 }
2928
2929 // Now elaborate the test with the same state, passing the target yield
2930 // values as arguments
2931 SmallVector<ElaboratorValue> blockArgs;
2932 Materializer materializer(OpBuilder::atBlockBegin(testOp.getBody()),
2933 testState, state, blockArgs);
2934
2935 for (auto [arg, val] :
2936 llvm::zip(testOp.getBody()->getArguments(), filteredYields))
2937 if (onlyLegalToMaterializeInTarget(arg.getType()))
2938 materializer.map(val, arg);
2939
2940 Elaborator elaborator(state, testState, materializer);
2941 SmallVector<ElaboratorValue> ignore;
2942 if (failed(elaborator.elaborate(testOp.getBodyRegion(), filteredYields,
2943 /*keepTerminator=*/false, ignore)))
2944 return failure();
2945
2946 materializer.finalize();
2947 }
2948
2949 return success();
2950}
assert(baseType &&"element must be base type")
static bool onlyLegalToMaterializeInTarget(Type type)
#define VISIT_UNSUPPORTED(STORAGETYPE)
static void print(TypedAttr val, llvm::raw_ostream &os)
static LogicalResult convert(arc::ExecuteOp op, arc::ExecuteOp::Adaptor adaptor, ConversionPatternRewriter &rewriter, const TypeConverter &converter)
static Location getLoc(DefSlot slot)
Definition Mem2Reg.cpp:222
static InstancePath empty
A namespace that is used to store existing names and generate new names in some scope within the IR.
Definition Namespace.h:30
This helps visit TypeOp nodes.
Definition RTGVisitors.h:25
ResultType visitExternalOp(Operation *op, ExtraArgs... args)
Definition RTGVisitors.h:95
ResultType visitUnhandledOp(Operation *op, ExtraArgs... args)
This callback is invoked on any operations that are not handled by the concrete visitor.
Direction get(bool isOutput)
Returns an output direction if isOutput is true, otherwise returns an input direction.
Definition CalyxOps.cpp:56
StringAttr getName(ArrayAttr names, size_t idx)
Return the name at the specified index of the ArrayAttr or null if it cannot be determined.
OS & operator<<(OS &os, const InnerSymTarget &target)
Printing InnerSymTarget's.
static llvm::hash_code hash_value(const ModulePort &port)
Definition HWTypes.h:39
int64_t getBitWidth(mlir::Type type)
Return the hardware bit width of a type.
Definition HWTypes.cpp:110
@ Delete
Erase the matched ops.
The InstanceGraph op interface, see InstanceGraphInterface.td for more details.
size_t hash_combine(size_t h1, size_t h2)
C++'s stdlib doesn't have a hash_combine function. This is a simple one.
Definition Utils.h:36
Definition rtg.py:1
Definition seq.py:1
static bool isEqual(const bool &lhs, const bool &rhs)
static unsigned getHashValue(const bool &val)