LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
FlatteningPass.cpp
Go to the documentation of this file.
1//===-- LLZKFlatteningPass.cpp - Implements -llzk-flatten pass --*- C++ -*-===//
2//
3// Part of the LLZK Project, under the Apache License v2.0.
4// See LICENSE.txt for license information.
5// Copyright 2025 Veridise Inc.
6// SPDX-License-Identifier: Apache-2.0
7//
8//===----------------------------------------------------------------------===//
13//===----------------------------------------------------------------------===//
14
29#include "llzk/Util/Concepts.h"
30#include "llzk/Util/Debug.h"
35
36#include <mlir/Dialect/Affine/IR/AffineOps.h>
37#include <mlir/Dialect/Affine/LoopUtils.h>
38#include <mlir/Dialect/Arith/IR/Arith.h>
39#include <mlir/Dialect/SCF/IR/SCF.h>
40#include <mlir/Dialect/SCF/Utils/Utils.h>
41#include <mlir/Dialect/Utils/StaticValueUtils.h>
42#include <mlir/IR/Attributes.h>
43#include <mlir/IR/BuiltinAttributes.h>
44#include <mlir/IR/BuiltinOps.h>
45#include <mlir/IR/BuiltinTypes.h>
46#include <mlir/Interfaces/InferTypeOpInterface.h>
47#include <mlir/Pass/PassManager.h>
48#include <mlir/Support/LLVM.h>
49#include <mlir/Support/LogicalResult.h>
50#include <mlir/Transforms/DialectConversion.h>
51#include <mlir/Transforms/GreedyPatternRewriteDriver.h>
52#include <mlir/Transforms/WalkPatternRewriteDriver.h>
53
54#include <llvm/ADT/APInt.h>
55#include <llvm/ADT/DenseMap.h>
56#include <llvm/ADT/DepthFirstIterator.h>
57#include <llvm/ADT/STLExtras.h>
58#include <llvm/ADT/SmallVector.h>
59#include <llvm/ADT/TypeSwitch.h>
60#include <llvm/Support/Debug.h>
61
62#include <cstdint>
63
64// Include the generated base pass class definitions.
65namespace llzk::polymorphic {
66#define GEN_PASS_DEF_FLATTENINGPASS
68} // namespace llzk::polymorphic
69
70#include "SharedImpl.h"
71
72#define DEBUG_TYPE "llzk-flatten"
73
74using namespace mlir;
75using namespace llzk;
76using namespace llzk::array;
77using namespace llzk::component;
78using namespace llzk::constrain;
79using namespace llzk::felt;
80using namespace llzk::function;
81using namespace llzk::polymorphic;
82using namespace llzk::polymorphic::detail;
83
84namespace {
85
86static void reportDelayedDiagnostics(CallOp caller, SmallVector<Diagnostic> &&diagnostics) {
87 DiagnosticEngine &engine = caller.getContext()->getDiagEngine();
88 for (Diagnostic &diag : diagnostics) {
89 // Update any notes referencing an UnknownLoc to use the CallOp location.
90 for (Diagnostic &note : diag.getNotes()) {
91 assert(note.getNotes().empty() && "notes cannot have notes attached");
92 if (llvm::isa<UnknownLoc>(note.getLocation())) {
93 note = std::move(Diagnostic(caller.getLoc(), note.getSeverity()).append(note.str()));
94 }
95 }
96 // Report. Based on InFlightDiagnostic::report().
97 engine.emit(std::move(diag));
98 }
99}
100
101class ConversionTracker {
106 struct PartialFuncInstantiation {
107 ArrayAttr concreteParamKey;
108 StringAttr templateName;
109 StringAttr functionName;
110 };
111
113 bool modified;
116 DenseMap<StructType, StructType> structInstantiations;
118 DenseMap<StructType, StructType> reverseInstantiations;
120 DenseSet<SymbolRefAttr> funcInstantiations;
123 DenseMap<Operation *, SmallVector<PartialFuncInstantiation>> partialFuncInstantiations;
124 /// Maps new remote type (i.e., the values in 'structInstantiations') to a list of Diagnostic
125 /// to report at the location(s) of the compute() that causes the instantiation to the StructType.
126 DenseMap<StructType, SmallVector<Diagnostic>> delayedDiagnostics;
127
128public:
129 bool isModified() const { return modified; }
130 void resetModifiedFlag() { modified = false; }
131 void updateModifiedFlag(bool currStepModified) { modified |= currStepModified; }
133 void recordInstantiation(StructType oldType, StructType newType) {
134 assert(!isNullOrEmpty(oldType.getParams()) && "cannot instantiate with no params");
135
136 auto forwardResult = structInstantiations.try_emplace(oldType, newType);
137 if (forwardResult.second) {
138 // Insertion was successful
139 // ASSERT: The reverse map does not contain this mapping either
140 assert(!reverseInstantiations.contains(newType));
141 reverseInstantiations[newType] = oldType;
142 // Set the modified flag
143 modified = true;
144 } else {
145 // ASSERT: If a mapping already existed for `oldType` it must be `newType`
146 assert(forwardResult.first->getSecond() == newType);
147 // ASSERT: The reverse mapping is already present as well
148 assert(reverseInstantiations.lookup(newType) == oldType);
149 }
150 assert(structInstantiations.size() == reverseInstantiations.size());
151 }
152
153
154 std::optional<StructType> getInstantiation(StructType oldType) const {
155 auto cachedResult = structInstantiations.find(oldType);
156 if (cachedResult != structInstantiations.end()) {
157 return cachedResult->second;
158 }
159 return std::nullopt;
161
163 void recordInstantiation(SymbolRefAttr funcName) {
164 funcInstantiations.insert(funcName);
165 modified = true;
166 }
167
169 std::optional<SymbolRefAttr>
170 lookupPartialFuncInstantiation(FuncDefOp sourceFunc, ArrayAttr concreteParamKey) const {
171 auto found = partialFuncInstantiations.find(sourceFunc.getOperation());
172 if (found == partialFuncInstantiations.end()) {
173 return std::nullopt;
174 }
175 for (const PartialFuncInstantiation &candidate : found->second) {
176 if (candidate.concreteParamKey == concreteParamKey) {
177 SmallVector<FlatSymbolRefAttr> calleeSuffix {
178 FlatSymbolRefAttr::get(candidate.templateName),
179 FlatSymbolRefAttr::get(candidate.functionName),
180 };
181 return asSymbolRefAttr(calleeSuffix);
182 }
183 }
184 return std::nullopt;
185 }
187 /// Publish a successful partial conversion after insertion and body conversion have completed.
188 void recordPartialFuncInstantiation(
189 FuncDefOp sourceFunc, ArrayAttr concreteParamKey, TemplateOp templateOp, FuncDefOp functionOp
190 ) {
191 assert(
192 !lookupPartialFuncInstantiation(sourceFunc, concreteParamKey).has_value() &&
193 "partial function instantiation already cached"
194 );
195 partialFuncInstantiations[sourceFunc.getOperation()].push_back(
196 PartialFuncInstantiation {
197 concreteParamKey,
198 templateOp.getSymNameAttr(),
199 functionOp.getSymNameAttr(),
200 }
201 );
202 }
203
204
206 void clearPartialFuncInstantiations() { partialFuncInstantiations.clear(); }
207
209 DenseSet<SymbolRefAttr> getInstantiatedDefinitionNames() const {
210 DenseSet<SymbolRefAttr> instantiatedNames = funcInstantiations;
211 for (const auto &[origRemoteTy, _] : structInstantiations) {
212 instantiatedNames.insert(origRemoteTy.getNameRef());
213 }
214 return instantiatedNames;
215 }
217 void reportDelayedDiagnostics(StructType newType, CallOp caller) {
218 auto res = delayedDiagnostics.find(newType);
219 if (res != delayedDiagnostics.end()) {
220 ::reportDelayedDiagnostics(caller, std::move(res->second));
221
222 // Emitting a Diagnostic consumes it (per DiagnosticEngine::emit) so remove them from the map.
223 // Unfortunately, this means if the key StructType is the result of instantiation at multiple
224 // `compute()` calls it will only be reported at one of those locations, not all.
225 delayedDiagnostics.erase(newType);
226 }
227 }
228
229 SmallVector<Diagnostic> &delayedDiagnosticSet(StructType newType) {
230 return delayedDiagnostics[newType];
231 }
232
235 bool isLegalConversion(Type oldType, Type newType, const char *patName) const {
236 std::function<bool(Type, Type)> checkInstantiations = [&](Type oTy, Type nTy) {
237 // Check if `oTy` is a struct with a known instantiation to `nTy`
238 if (StructType oldStructType = llvm::dyn_cast<StructType>(oTy)) {
239 // Note: The values in `structInstantiations` must be no-parameter struct types
240 // so there is no need for recursive check, simple equality is sufficient.
241 if (this->structInstantiations.lookup(oldStructType) == nTy) {
242 return true;
243 }
244 }
245 // Check if `nTy` is the result of a struct instantiation and if the pre-image of
246 // that instantiation (i.e., the parameterized version of the instantiated struct)
247 // is a more concrete unification of `oTy`.
248 if (StructType newStructType = llvm::dyn_cast<StructType>(nTy)) {
249 if (auto preImage = this->reverseInstantiations.lookup(newStructType)) {
250 if (isMoreConcreteUnification(oTy, preImage, checkInstantiations)) {
251 return true;
252 }
253 }
254 }
255 return false;
256 };
257
258 if (isMoreConcreteUnification(oldType, newType, checkInstantiations)) {
259 return true;
260 }
261 LLVM_DEBUG(
262 llvm::dbgs() << "[" << patName << "] Cannot replace old type " << oldType
263 << " with new type " << newType
264 << " because it does not define a compatible and more concrete type.\n";
265 );
266 return false;
267 }
268
269 template <typename T, typename U>
270 inline bool areLegalConversions(T oldTypes, U newTypes, const char *patName) const {
271 return llvm::all_of(
272 llvm::zip_equal(oldTypes, newTypes), [this, &patName](std::tuple<Type, Type> oldThenNew) {
273 return this->isLegalConversion(std::get<0>(oldThenNew), std::get<1>(oldThenNew), patName);
274 }
275 );
276 }
277};
278
279template <typename Impl, typename Op, typename... HandledAttrs>
280class SymbolUserHelper : public OpConversionPattern<Op> {
281private:
282 const DenseMap<Attribute, Attribute> &paramNameToValue;
283
284 SymbolUserHelper(
285 TypeConverter &converter, MLIRContext *ctx, unsigned patternBenefit,
286 const DenseMap<Attribute, Attribute> &paramNameToInstantiatedValue
287 )
288 : OpConversionPattern<Op>(converter, ctx, patternBenefit),
289 paramNameToValue(paramNameToInstantiatedValue) {}
290
291public:
292 using OpAdaptor = typename mlir::OpConversionPattern<Op>::OpAdaptor;
293
294 virtual Attribute getNameAttr(Op) const = 0;
295
296 virtual LogicalResult handleDefaultRewrite(
297 Attribute, Op op, OpAdaptor, ConversionPatternRewriter &, Attribute a
298 ) const {
299 return op->emitOpError().append("expected value with type ", op.getType(), " but found ", a);
300 }
301
302 LogicalResult
303 matchAndRewrite(Op op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) const override {
304 LLVM_DEBUG(llvm::dbgs() << "[SymbolUserHelper] op: " << op << '\n');
305 auto res = this->paramNameToValue.find(getNameAttr(op));
306 if (res == this->paramNameToValue.end()) {
307 LLVM_DEBUG(llvm::dbgs() << "[SymbolUserHelper] no instantiation for " << op << '\n');
308 return failure();
309 }
310 llvm::TypeSwitch<Attribute, LogicalResult> TS(res->second);
311 llvm::TypeSwitch<Attribute, LogicalResult> *ptr = &TS;
312
313 ((ptr = &(ptr->template Case<HandledAttrs>([&](HandledAttrs a) {
314 return static_cast<const Impl *>(this)->handleRewrite(res->first, op, adaptor, rewriter, a);
315 }))),
316 ...);
317
318 return TS.Default([&](Attribute a) {
319 return handleDefaultRewrite(res->first, op, adaptor, rewriter, a);
320 });
321 }
322 friend Impl;
323};
324
325class ClonedBodyConstReadOpPattern
326 : public SymbolUserHelper<
327 ClonedBodyConstReadOpPattern, ConstReadOp, IntegerAttr, FeltConstAttr> {
328 SmallVector<Diagnostic> &diagnostics;
329
330 using super =
331 SymbolUserHelper<ClonedBodyConstReadOpPattern, ConstReadOp, IntegerAttr, FeltConstAttr>;
332
333public:
334 ClonedBodyConstReadOpPattern(
335 TypeConverter &converter, MLIRContext *ctx,
336 const DenseMap<Attribute, Attribute> &paramNameToInstantiatedValue,
337 SmallVector<Diagnostic> &instantiationDiagnostics
338 )
339 // benefit>0 so this applies instead of GeneralTypeReplacePattern<ConstReadOp>
340 : super(converter, ctx, /*patternBenefit=*/1, paramNameToInstantiatedValue),
341 diagnostics(instantiationDiagnostics) {}
342
343 Attribute getNameAttr(ConstReadOp op) const override { return op.getConstNameAttr(); }
344
345 LogicalResult handleRewrite(
346 Attribute sym, ConstReadOp op, OpAdaptor, ConversionPatternRewriter &rewriter, IntegerAttr a
347 ) const {
348 APInt attrValue = a.getValue();
349 Type origResTy = op.getType();
350 Type newResTy = getTypeConverter()->convertType(origResTy);
351 if (!newResTy) {
352 return op->emitOpError().append("could not convert result type ", origResTy);
353 }
354
355 if (FeltType ty = llvm::dyn_cast<FeltType>(newResTy)) {
357 rewriter, op, FeltConstAttr::get(getContext(), attrValue, ty)
358 );
359 return success();
360 }
361
362 if (llvm::isa<IndexType>(newResTy)) {
364 return success();
365 }
366
367 if (newResTy.isSignlessInteger(1)) {
368 // Treat 0 as false and any other value as true (but give a warning if it's not 1)
369 if (attrValue.isZero()) {
370 replaceOpWithNewOp<arith::ConstantIntOp>(rewriter, op, false, newResTy);
371 return success();
372 }
373 if (!attrValue.isOne()) {
374 Location opLoc = op.getLoc();
375 Diagnostic diag(opLoc, DiagnosticSeverity::Warning);
376 diag << "Interpreting non-zero value " << stringWithoutType(a) << " as true";
377 if (getContext()->shouldPrintOpOnDiagnostic()) {
378 diag.attachNote(opLoc) << "see current operation: " << *op;
379 }
380 diag.attachNote(UnknownLoc::get(getContext()))
381 << "when instantiating '" << StructDefOp::getOperationName() << "' parameter \"" << sym
382 << "\" for this call";
383 diagnostics.push_back(std::move(diag));
384 }
385 replaceOpWithNewOp<arith::ConstantIntOp>(rewriter, op, true, newResTy);
386 return success();
387 }
388 return op->emitOpError().append("unexpected result type ", newResTy);
389 }
390
391 LogicalResult handleRewrite(
392 Attribute, ConstReadOp op, OpAdaptor, ConversionPatternRewriter &rewriter, FeltConstAttr a
393 ) const {
394 replaceOpWithNewOp<FeltConstantOp>(rewriter, op, a);
395 return success();
396 }
397};
398
401struct MatchFailureListener : public RewriterBase::Listener {
402 bool hadFailure = false;
403
404 ~MatchFailureListener() override {}
405
406 void notifyMatchFailure(Location loc, function_ref<void(Diagnostic &)> reasonCallback) override {
407 hadFailure = true;
408
409 InFlightDiagnostic diag = emitError(loc);
410 reasonCallback(*diag.getUnderlyingDiagnostic());
411 diag.report();
412 }
413};
414
415static LogicalResult
416applyAndFoldGreedily(ModuleOp modOp, ConversionTracker &tracker, RewritePatternSet &&patterns) {
417 bool currStepModified = false;
418 MatchFailureListener failureListener;
419 LogicalResult result = applyPatternsGreedily(
420 modOp->getRegion(0), std::move(patterns),
421 GreedyRewriteConfig {.maxIterations = 20, .listener = &failureListener, .fold = true},
422 &currStepModified
423 );
424 tracker.updateModifiedFlag(currStepModified);
425 return failure(result.failed() || failureListener.hadFailure);
426}
427
429template <bool AllowStructParams = true> bool isConcreteAttr(Attribute a) {
430 return classifyAttrConcreteness(a, AllowStructParams) == AttrConcreteness::Concrete;
431}
432
433static SymbolRefAttr
434convertCalleeSymRefs(SymbolRefAttr callee, const DenseMap<Attribute, Attribute> &paramNameToValue) {
435 auto it = paramNameToValue.find(FlatSymbolRefAttr::get(callee.getRootReference()));
436 if (it == paramNameToValue.end()) {
437 return callee;
438 }
439
440 auto tyAttr = llvm::dyn_cast<TypeAttr>(it->second);
441 if (!tyAttr) {
442 return callee;
443 }
444
445 auto structTy = llvm::dyn_cast<StructType>(tyAttr.getValue());
446 if (!structTy) {
447 return callee;
448 }
449
450 SmallVector<FlatSymbolRefAttr> newPieces = getPieces(structTy.getNameRef());
451 llvm::append_range(newPieces, callee.getNestedReferences());
452 return asSymbolRefAttr(newPieces);
453}
454
455static void
456convertCalleesInPlace(Operation *op, const DenseMap<Attribute, Attribute> &paramNameToValue) {
457 op->walk([&paramNameToValue](CallOp callOp) {
458 callOp.setCalleeAttr(convertCalleeSymRefs(callOp.getCalleeAttr(), paramNameToValue));
459 });
460}
461
462static bool calleeReferencesTemplateParam(CallOp op) {
463 SymbolRefAttr callee = op.getCalleeAttr();
464 if (!callee || callee.getNestedReferences().size() != 1) {
465 return false;
466 }
467 TemplateOp parentTemplate = getParentOfType<TemplateOp>(op);
468 if (!parentTemplate) {
469 return false;
470 }
471 return parentTemplate.hasConstNamed<TemplateParamOp>(callee.getRootReference());
472}
473
478static std::optional<Attribute>
479evaluateExpr(TemplateExprOp exprOp, const DenseMap<Attribute, Attribute> &paramNameToConcrete) {
480 // Map from SSA value in the expr body to its concrete Attribute.
481 DenseMap<Value, Attribute> valueMap;
482 for (Operation &bodyOp : exprOp.getInitializerRegion().front()) {
483 if (auto yieldOp = llvm::dyn_cast<YieldOp>(bodyOp)) {
484 auto it = valueMap.find(yieldOp.getVal());
485 return it != valueMap.end() ? std::make_optional(it->second) : std::nullopt;
486 }
487
488 if (auto constReadOp = llvm::dyn_cast<ConstReadOp>(bodyOp)) {
489 auto it = paramNameToConcrete.find(constReadOp.getConstNameAttr());
490 if (it == paramNameToConcrete.end()) {
491 return std::nullopt; // a referenced param is not concrete
492 }
493 // If the attribute type is `FeltType` but it's stored as an IntegerAttr, promote to
494 // a `FeltConstAttr`.
495 Attribute val = it->second;
496 if (auto intAttr = llvm::dyn_cast<IntegerAttr>(val)) {
497 if (auto feltTy = llvm::dyn_cast<FeltType>(constReadOp.getResult().getType())) {
498 val = FeltConstAttr::get(bodyOp.getContext(), intAttr.getValue(), feltTy);
499 }
500 }
501 valueMap[constReadOp.getResult()] = val;
502 continue;
503 }
504
505 // Gather constant attributes for all operands.
506 SmallVector<Attribute> operandAttrs;
507 operandAttrs.reserve(bodyOp.getNumOperands());
508 for (Value operand : bodyOp.getOperands()) {
509 auto it = valueMap.find(operand);
510 if (it == valueMap.end()) {
511 return std::nullopt; // operand not known as a constant
512 }
513 operandAttrs.push_back(it->second);
514 }
515
516 // Try constant folding.
517 SmallVector<OpFoldResult> foldResults;
518 if (succeeded(bodyOp.fold(operandAttrs, foldResults)) &&
519 foldResults.size() == bodyOp.getNumResults()) {
520 for (auto [result, fr] : llvm::zip_equal(bodyOp.getResults(), foldResults)) {
521 if (Attribute a = llvm::dyn_cast<Attribute>(fr)) {
522 valueMap[result] = a;
523 } else {
524 return std::nullopt;
525 }
526 }
527 }
528 }
529 return std::nullopt; // no YieldOp found (shouldn't happen in a valid expr)
530}
531
535static void
536evaluateTemplateExprs(TemplateOp templateOp, DenseMap<Attribute, Attribute> &paramNameToConcrete) {
537 LLVM_DEBUG(
538 llvm::dbgs() << "[evaluateTemplateExprs] before: " << debug::toStringList(paramNameToConcrete)
539 << '\n'
540 );
541 for (TemplateExprOp exprOp : templateOp.getConstOps<TemplateExprOp>()) {
542 std::optional<Attribute> result = evaluateExpr(exprOp, paramNameToConcrete);
543 if (result.has_value()) {
544 auto exprNameAttr = FlatSymbolRefAttr::get(exprOp.getSymNameAttr());
545 paramNameToConcrete.try_emplace(exprNameAttr, *result);
546 LLVM_DEBUG(
547 llvm::dbgs() << "[evaluateTemplateExprs] expr @" << exprOp.getSymName()
548 << " evaluated to " << *result << '\n'
549 );
550 }
551 }
552 LLVM_DEBUG(
553 llvm::dbgs() << "[evaluateTemplateExprs] after: " << debug::toStringList(paramNameToConcrete)
554 << '\n'
555 );
556}
557
558static inline bool tableOffsetIsntSymbol(MemberReadOp op) {
559 return !llvm::isa_and_present<SymbolRefAttr>(op.getTableOffset().value_or(nullptr));
560}
561
564class ClonedMemberReadOpPattern
565 : public SymbolUserHelper<ClonedMemberReadOpPattern, MemberReadOp, IntegerAttr> {
566 using super = SymbolUserHelper<ClonedMemberReadOpPattern, MemberReadOp, IntegerAttr>;
567
568public:
569 ClonedMemberReadOpPattern(
570 TypeConverter &converter, MLIRContext *ctx,
571 const DenseMap<Attribute, Attribute> &paramNameToInstantiatedValue
572 )
573 // benefit>0 so this applies instead of GeneralTypeReplacePattern<MemberReadOp>
574 : super(converter, ctx, /*patternBenefit=*/1, paramNameToInstantiatedValue) {}
575
576 Attribute getNameAttr(MemberReadOp op) const override {
577 return op.getTableOffset().value_or(nullptr);
578 }
579
580 LogicalResult handleRewrite(
581 Attribute, MemberReadOp op, OpAdaptor, ConversionPatternRewriter &rewriter, IntegerAttr a
582 ) const {
583 rewriter.modifyOpInPlace(op, [&]() {
584 op.setTableOffsetAttr(rewriter.getIndexAttr(fromAPInt(a.getValue())));
585 });
586
587 return success();
588 }
589
590 LogicalResult handleDefaultRewrite(
591 Attribute, MemberReadOp op, OpAdaptor, ConversionPatternRewriter &, Attribute a
592 ) const override {
593 return op->emitOpError().append(
594 "table offset requires an integer template value, but found ", a
595 );
596 }
597
598 LogicalResult matchAndRewrite(
599 MemberReadOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter
600 ) const override {
601 LLVM_DEBUG(llvm::dbgs() << "[ClonedMemberReadOpPattern] MemberReadOp: " << op << '\n';);
602 if (tableOffsetIsntSymbol(op)) {
603 return failure();
604 }
605
606 return super::matchAndRewrite(op, adaptor, rewriter);
607 }
608};
609
611
614class StructCloner {
615 ConversionTracker &tracker_;
616 ModuleOp rootMod;
617 SymbolTableCollection symTables;
618 bool reportMissing = true;
619
620 class MappedTypeConverter : public TypeConverter {
621 StructType origTy;
622 StructType newTy;
623 const DenseMap<Attribute, Attribute> &paramNameToValue;
624
625 inline Attribute convertIfPossible(Attribute a) const {
626 auto res = this->paramNameToValue.find(a);
627 return (res != this->paramNameToValue.end()) ? res->second : a;
628 }
629
630 public:
631 MappedTypeConverter(
632 StructType originalType, StructType newType,
634 const DenseMap<Attribute, Attribute> &paramNameToInstantiatedValue
635 )
636 : TypeConverter(), origTy(originalType), newTy(newType),
637 paramNameToValue(paramNameToInstantiatedValue) {
638
639 addConversion([](Type inputTy) { return inputTy; });
640
641 addConversion([this](StructType inputTy) {
642 LLVM_DEBUG(llvm::dbgs() << "[MappedTypeConverter] convert " << inputTy << '\n');
643
644 // Check for replacement of the full type
645 if (inputTy == this->origTy) {
646 return this->newTy;
647 }
648 // Check for replacement of parameter symbol names with concrete values
649 if (ArrayAttr inputTyParams = inputTy.getParams()) {
650 SmallVector<Attribute> updated;
651 for (Attribute a : inputTyParams) {
652 if (TypeAttr ta = dyn_cast<TypeAttr>(a)) {
653 updated.push_back(TypeAttr::get(this->convertType(ta.getValue())));
654 } else {
655 updated.push_back(convertIfPossible(a));
656 }
657 }
658 return getStructTypeWithParams(inputTy.getNameRef(), inputTy.getContext(), updated);
659 }
660 // Otherwise, return the type unchanged
661 return inputTy;
662 });
663
664 addConversion([this](ArrayType inputTy) {
665 // Check for replacement of parameter symbol names with concrete values
666 ArrayRef<Attribute> dimSizes = inputTy.getDimensionSizes();
667 if (!dimSizes.empty()) {
668 SmallVector<Attribute> updated;
669 for (Attribute a : dimSizes) {
670 updated.push_back(convertIfPossible(a));
671 }
672 return ArrayType::get(this->convertType(inputTy.getElementType()), updated);
673 }
674 // Otherwise, return the type unchanged
675 return inputTy;
676 });
677
678 addConversion([this](TypeVarType inputTy) -> Type {
679 // Check for replacement of parameter symbol name with a concrete type
680 if (TypeAttr tyAttr = llvm::dyn_cast<TypeAttr>(convertIfPossible(inputTy.getNameRef()))) {
681 Type convertedType = tyAttr.getValue();
682 // Use the new type unless it contains a TypeVarType because a TypeVarType from a
683 // different struct references a parameter name from that other struct, not from the
684 // current struct so the reference would be invalid.
685 if (isConcreteType(convertedType)) {
686 return convertedType;
687 }
688 }
689 return inputTy;
690 });
691 }
692 };
693
694 FailureOr<StructType> genClone(StructType typeAtCaller, ArrayRef<Attribute> typeAtCallerParams) {
695 LLVM_DEBUG(llvm::dbgs() << "[StructCloner] attempting clone of " << typeAtCaller << '\n');
696 // Find the StructDefOp for the original StructType
697 FailureOr<SymbolLookupResult<StructDefOp>> r =
698 typeAtCaller.getDefinition(symTables, rootMod, reportMissing);
699 if (failed(r)) {
700 LLVM_DEBUG(llvm::dbgs() << "[StructCloner] skip: cannot find StructDefOp \n");
701 return failure(); // getDefinition() already emits a sufficient error message
702 }
703 LLVM_DEBUG(llvm::dbgs() << "[StructCloner] found definition\n";);
704
705 StructDefOp origStruct = r->get();
706 StructType typeAtDef = origStruct.getType();
707 MLIRContext *ctx = origStruct.getContext();
708 TemplateOp parentTemplate = getParentOfType<TemplateOp>(origStruct);
709 assert(parentTemplate && "parameterized struct must be nested in a TemplateOp");
710 ModuleOp parentModule = getParentOfType<ModuleOp>(parentTemplate);
711 assert(parentModule && "TemplateOp must be nested in a ModuleOp");
712
713 // Map of StructDefOp parameter name to concrete Attribute at the current instantiation site.
714 DenseMap<Attribute, Attribute> paramNameToConcrete;
715 // Reduced from `typeAtCallerParams` to contain only the non-concrete Attributes.
716 ArrayAttr reducedCallerParams = nullptr;
717 SmallVector<Attribute> nonConcreteParams;
718 {
719 ArrayAttr paramNames = typeAtDef.getParams();
720
721 // pre-conditions
722 assert(!isNullOrEmpty(paramNames));
723 assert(paramNames.size() == typeAtCallerParams.size());
724
725 for (size_t i = 0, e = paramNames.size(); i < e; ++i) {
726 Attribute next = typeAtCallerParams[i];
727 if (isConcreteAttr<false>(next)) {
728 paramNameToConcrete[paramNames[i]] = next;
729 } else {
730 nonConcreteParams.push_back(next);
731 }
732 }
733 // post-conditions
734 assert(nonConcreteParams.size() + paramNameToConcrete.size() == paramNames.size());
735
736 if (paramNameToConcrete.empty()) {
737 LLVM_DEBUG(llvm::dbgs() << "[StructCloner] skip: no concrete params \n");
738 return failure();
739 }
740 if (!nonConcreteParams.empty()) {
741 reducedCallerParams = ArrayAttr::get(ctx, nonConcreteParams);
742 }
743 }
744
745 FailureOr<InstantiationLayout> layoutResult =
746 buildInstantiationLayout(parentTemplate, ArrayAttr(), paramNameToConcrete);
747 if (failed(layoutResult)) {
748 return failure();
749 }
750 InstantiationLayout layout = std::move(*layoutResult);
751 assert(layout.remainingNames.size() == nonConcreteParams.size());
752
753 // This list will be used to build the new remote/external type.
754 SmallVector<FlatSymbolRefAttr> typeAtCallerSymPieces = getPieces(typeAtCaller.getNameRef());
755 typeAtCallerSymPieces.pop_back(); // drop struct name
756
757 // Evaluate any poly.expr symbols whose param dependencies are now concrete; add them to the
758 // map so ClonedBodyConstReadOpPattern can replace uses of those symbols too.
759 evaluateTemplateExprs(parentTemplate, paramNameToConcrete);
760
761 // Clone the original struct.
762 StructDefOp newStruct = origStruct.clone();
763 convertCalleesInPlace(newStruct, paramNameToConcrete);
764 if (layout.remainingNames.empty()) { // FULL INSTANTIATION CASE
765 // Set name of the new struct by prepending its name with instantiated template name.
766 newStruct.setSymName(
767 (layout.templateNameWithAttrs + mlir::Twine('_') + newStruct.getSymName()).str()
768 );
769 // Insert 'newStruct' into the parent ModuleOp of the original TemplateOp. Use the
770 // `SymbolTable::insert()` function so that the name will be made unique if necessary.
771 symTables.getSymbolTable(parentModule).insert(newStruct, Block::iterator(parentTemplate));
772 // Drop the old template name from the list.
773 typeAtCallerSymPieces.pop_back();
774 } else { // PARTIAL INSTANTIATION CASE
775 // Clone the template and set instantiated name.
776 TemplateOp newTemplate = parentTemplate.cloneWithoutRegions();
777 newTemplate.setSymName(layout.templateNameWithAttrs);
778 setInstantiationNamePattern(newTemplate, layout.namePattern);
779 assert(newTemplate->getNumRegions() > 0 && "region exists"); // it just doesn't have a block
780 newTemplate.getBodyRegion().emplaceBlock();
781
782 // Clone preserved const param/expr ops.
783 for (Attribute name : layout.remainingNames) {
784 FlatSymbolRefAttr nameSym = llvm::dyn_cast<FlatSymbolRefAttr>(name);
785 assert(nameSym && "expected FlatSymbolRefAttr");
786
787 Operation *symOp = symTables.getSymbolTable(parentTemplate).lookup(nameSym.getAttr());
788 assert(symOp && "symbol must exist");
789 newTemplate.insert(newTemplate.begin(), symOp->clone());
790 }
791
792 // Insert the struct into the template and the template into the module. Use the
793 // `SymbolTable::insert()` function so that the name will be made unique if necessary.
794 symTables.getSymbolTable(newTemplate).insert(newStruct);
795 symTables.getSymbolTable(parentModule).insert(newTemplate, Block::iterator(parentTemplate));
796
797 // Replace the old template name in the list with the new one (get template name after
798 // symbol table insertion since it may be modified to make it unique).
799 typeAtCallerSymPieces.back() = FlatSymbolRefAttr::get(newTemplate.getSymNameAttr());
800 }
801
802 // Retrieve the new type AFTER inserting since the struct name may be appended to make
803 // it unique and use the remaining non-concrete parameters from the original type.
804 StructType newLocalType = newStruct.getType(reducedCallerParams);
805 typeAtCallerSymPieces.push_back(
806 FlatSymbolRefAttr::get(newLocalType.getNameRef().getLeafReference())
807 );
808 StructType newRemoteType =
809 StructType::get(asSymbolRefAttr(typeAtCallerSymPieces), newLocalType.getParams());
810 LLVM_DEBUG({
811 llvm::dbgs() << "[StructCloner] original def type: " << typeAtDef << '\n';
812 llvm::dbgs() << "[StructCloner] cloned def type: " << newStruct.getType() << '\n';
813 llvm::dbgs() << "[StructCloner] original remote type: " << typeAtCaller << '\n';
814 llvm::dbgs() << "[StructCloner] cloned local type: " << newLocalType << '\n';
815 llvm::dbgs() << "[StructCloner] cloned remote type: " << newRemoteType << '\n';
816 });
817
818 // Within the new struct, replace all references to the original StructType (i.e., the
819 // locally-parameterized version) with the new locally-parameterized StructType,
820 // and replace all uses of the removed struct parameters with the concrete values.
821 MappedTypeConverter tyConv(typeAtDef, newStruct.getType(), paramNameToConcrete);
822 ConversionTarget target =
823 newConverterDefinedTarget<EmitEqualityOp>(tyConv, ctx, tableOffsetIsntSymbol);
824 target.addDynamicallyLegalOp<ConstReadOp>([&paramNameToConcrete](ConstReadOp op) {
825 // Legal if it's not in the map of concrete attribute instantiations
826 return !paramNameToConcrete.contains(op.getConstNameAttr());
827 });
828
829 RewritePatternSet patterns = newGeneralRewritePatternSet<EmitEqualityOp>(tyConv, ctx, target);
830 patterns.add<ClonedBodyConstReadOpPattern>(
831 tyConv, ctx, paramNameToConcrete, tracker_.delayedDiagnosticSet(newLocalType)
832 );
833 patterns.add<ClonedMemberReadOpPattern>(tyConv, ctx, paramNameToConcrete);
834 if (failed(applyFullConversion(newStruct, target, std::move(patterns)))) {
835 LLVM_DEBUG(llvm::dbgs() << "[StructCloner] instantiating body of struct failed \n");
836 return failure();
837 }
838 return newRemoteType;
839 }
840
841public:
842 StructCloner(ConversionTracker &tracker, ModuleOp root)
843 : tracker_(tracker), rootMod(root), symTables() {}
844
845 FailureOr<StructType> createInstantiatedClone(StructType orig) {
846 LLVM_DEBUG(llvm::dbgs() << "[StructCloner] orig: " << orig << '\n');
847 if (ArrayAttr params = orig.getParams()) {
848 return genClone(orig, params.getValue());
849 }
850 LLVM_DEBUG(llvm::dbgs() << "[StructCloner] skip: nullptr for params \n");
851 return failure();
852 }
853
854 void enableReportMissing() { reportMissing = true; }
855
856 void disableReportMissing() { reportMissing = false; }
857};
858
859class DisableReportMissing;
860
861class ParameterizedStructUseTypeConverter : public TypeConverter {
862 ConversionTracker &tracker_;
863 StructCloner cloner;
864
865 friend DisableReportMissing;
866
867public:
868 ParameterizedStructUseTypeConverter(ConversionTracker &tracker, ModuleOp root)
869 : TypeConverter(), tracker_(tracker), cloner(tracker, root) {
870
871 addConversion([](Type inputTy) { return inputTy; });
872
873 addConversion([this](StructType inputTy) -> StructType {
874 LLVM_DEBUG(
875 llvm::dbgs() << "[ParameterizedStructUseTypeConverter] attempting conversion of "
876 << inputTy << '\n';
877 );
878 // First check for a cached entry
879 if (auto opt = tracker_.getInstantiation(inputTy)) {
880 return opt.value();
881 }
882
883 // Otherwise, try to create a clone of the struct with instantiated params. If that can't be
884 // done, return the original type to indicate that it's still legal (for this step at least).
885 FailureOr<StructType> cloneRes = cloner.createInstantiatedClone(inputTy);
886 if (failed(cloneRes)) {
887 return inputTy;
888 }
889 StructType newTy = cloneRes.value();
890 LLVM_DEBUG(
891 llvm::dbgs() << "[ParameterizedStructUseTypeConverter] instantiating " << inputTy
892 << " as " << newTy << '\n'
893 );
894 tracker_.recordInstantiation(inputTy, newTy);
895 return newTy;
896 });
897
898 addConversion([this](ArrayType inputTy) {
899 return inputTy.cloneWith(convertType(inputTy.getElementType()));
900 });
901 }
902};
903
904class CallStructFuncPattern : public OpConversionPattern<CallOp> {
905 ConversionTracker &tracker_;
906
907public:
908 CallStructFuncPattern(TypeConverter &converter, MLIRContext *ctx, ConversionTracker &tracker)
909 // benefit>0 so this applies instead of CallOpClassReplacePattern
910 : OpConversionPattern<CallOp>(converter, ctx, /*benefit=*/1), tracker_(tracker) {}
911
912 LogicalResult matchAndRewrite(
913 CallOp op, OpAdaptor adapter, ConversionPatternRewriter &rewriter
914 ) const override {
915 LLVM_DEBUG(llvm::dbgs() << "[CallStructFuncPattern] CallOp: " << op << '\n');
916
917 // Convert the result types of the CallOp
918 SmallVector<Type> newResultTypes;
919 if (failed(getTypeConverter()->convertTypes(op.getResultTypes(), newResultTypes))) {
920 return op->emitError("Could not convert Op result types.");
921 }
922 LLVM_DEBUG({
923 llvm::dbgs() << "[CallStructFuncPattern] newResultTypes: "
924 << debug::toStringList(newResultTypes) << '\n';
925 });
926
927 // Update the callee to reflect the new struct target if necessary. These checks are based on
928 // `CallOp::calleeIsStructC*()` but the types must not come from the CallOp in this case.
929 // Instead they must come from the converted versions.
930 SymbolRefAttr calleeAttr = op.getCalleeAttr();
931 if (op.calleeIsStructCompute()) {
932 if (StructType newStTy = getIfSingleton<StructType>(newResultTypes)) {
933 LLVM_DEBUG(llvm::dbgs() << "[CallStructFuncPattern] newStTy: " << newStTy << '\n');
934 calleeAttr = appendLeaf(newStTy.getNameRef(), calleeAttr.getLeafReference());
935 tracker_.reportDelayedDiagnostics(newStTy, op);
936 }
937 } else if (op.calleeIsStructConstrain()) {
938 if (StructType newStTy = getAtIndex<StructType>(adapter.getArgOperands().getTypes(), 0)) {
939 LLVM_DEBUG(llvm::dbgs() << "[CallStructFuncPattern] newStTy: " << newStTy << '\n');
940 calleeAttr = appendLeaf(newStTy.getNameRef(), calleeAttr.getLeafReference());
941 }
942 }
943
944 LLVM_DEBUG(llvm::dbgs() << "[CallStructFuncPattern] replaced " << op);
946 rewriter, op, newResultTypes, calleeAttr, adapter.getMapOperands(),
947 op.getNumDimsPerMapAttr(), adapter.getArgOperands()
948 );
949 (void)newOp; // tell compiler it's intentionally unused in release builds
950 LLVM_DEBUG(llvm::dbgs() << " with " << newOp << '\n');
951 return success();
952 }
953};
954
955// This one ensures MemberDefOp types are converted even if there are no reads/writes to them.
956class MemberDefOpPattern : public OpConversionPattern<MemberDefOp> {
957public:
958 MemberDefOpPattern(TypeConverter &converter, MLIRContext *ctx, ConversionTracker &)
959 // benefit>0 so this applies instead of GeneralTypeReplacePattern<MemberDefOp>
960 : OpConversionPattern<MemberDefOp>(converter, ctx, /*benefit=*/1) {}
961
962 LogicalResult matchAndRewrite(
963 MemberDefOp op, OpAdaptor /*adapter*/, ConversionPatternRewriter &rewriter
964 ) const override {
965 LLVM_DEBUG(llvm::dbgs() << "[MemberDefOpPattern] MemberDefOp: " << op << '\n');
966
967 Type oldMemberType = op.getType();
968 Type newMemberType = getTypeConverter()->convertType(oldMemberType);
969 if (oldMemberType == newMemberType) {
970 return failure(); // nothing changed
971 }
972 rewriter.modifyOpInPlace(op, [&op, &newMemberType]() { op.setType(newMemberType); });
973 return success();
974 }
975};
976
979class DisableReportMissing : public LegalityCheckCallback {
980 ParameterizedStructUseTypeConverter &tyConv;
981
982public:
983 explicit DisableReportMissing(ParameterizedStructUseTypeConverter &tc) : tyConv(tc) {}
984
985 void checkStarted() override { tyConv.cloner.disableReportMissing(); }
986
987 void checkEnded(bool) override { tyConv.cloner.enableReportMissing(); }
988};
989
990LogicalResult run(ModuleOp modOp, ConversionTracker &tracker) {
991 MLIRContext *ctx = modOp.getContext();
992 ParameterizedStructUseTypeConverter tyConv(tracker, modOp);
993 DisableReportMissing drm(tyConv);
994 ConversionTarget target = newConverterDefinedTargetWithCallback<>(tyConv, ctx, drm);
995 RewritePatternSet patterns = newGeneralRewritePatternSet(tyConv, ctx, target);
996 patterns.add<CallStructFuncPattern, MemberDefOpPattern>(tyConv, ctx, tracker);
997 return applyPartialConversion(modOp, target, std::move(patterns));
998}
999
1002LogicalResult instantiateMainStruct(ModuleOp modOp, ConversionTracker &tracker) {
1003 FailureOr<StructType> mainTypeOpt = getMainInstanceType(modOp);
1004 if (failed(mainTypeOpt)) {
1005 return failure();
1006 }
1007
1008 StructType mainType = mainTypeOpt.value();
1009 if (!mainType || isNullOrEmpty(mainType.getParams()) || tracker.getInstantiation(mainType)) {
1010 return success();
1011 }
1012
1013 StructCloner cloner(tracker, modOp);
1014 FailureOr<StructType> cloneRes = cloner.createInstantiatedClone(mainType);
1015 if (failed(cloneRes)) {
1016 return failure();
1017 }
1018
1019 StructType instantiatedMainType = cloneRes.value();
1020 tracker.recordInstantiation(mainType, instantiatedMainType);
1021 modOp->setAttr(MAIN_ATTR_NAME, TypeAttr::get(instantiatedMainType));
1022 return success();
1023}
1024
1025} // namespace Step1_InstantiateStructs
1026
1028
1031class FuncInstTypeConverter : public TypeConverter {
1032 DenseMap<Attribute, Attribute> paramNameToValue;
1033
1034 Attribute convertIfPossible(Attribute a) const {
1035 auto res = paramNameToValue.find(a);
1036 return (res != paramNameToValue.end()) ? res->second : a;
1037 }
1038
1039public:
1040 explicit FuncInstTypeConverter(DenseMap<Attribute, Attribute> paramNameToConcrete)
1041 : TypeConverter(), paramNameToValue(std::move(paramNameToConcrete)) {
1042 addConversion([](Type t) { return t; });
1043
1044 addConversion([this](TypeVarType inputTy) -> Type {
1045 if (TypeAttr tyAttr = llvm::dyn_cast<TypeAttr>(convertIfPossible(inputTy.getNameRef()))) {
1046 Type convertedType = tyAttr.getValue();
1047 if (isConcreteType(convertedType)) {
1048 return convertedType;
1049 }
1050 }
1051 return inputTy;
1052 });
1053
1054 addConversion([this](ArrayType inputTy) {
1055 SmallVector<Attribute> updated;
1056 bool changed = false;
1057 for (Attribute a : inputTy.getDimensionSizes()) {
1058 Attribute converted = convertIfPossible(a);
1059 updated.push_back(converted);
1060 if (converted != a) {
1061 changed = true;
1062 }
1063 }
1064 Type newElemTy = this->convertType(inputTy.getElementType());
1065 if (!changed && newElemTy == inputTy.getElementType()) {
1066 return inputTy;
1067 }
1069 inputTy.cloneWith(inputTy.getElementType(), updated), newElemTy
1070 );
1071 });
1072
1073 addConversion([this](StructType inputTy) -> StructType {
1074 if (ArrayAttr params = inputTy.getParams()) {
1075 SmallVector<Attribute> updated;
1076 bool changed = false;
1077 for (Attribute a : params) {
1078 if (TypeAttr ta = dyn_cast<TypeAttr>(a)) {
1079 Type newTy = this->convertType(ta.getValue());
1080 if (newTy != ta.getValue()) {
1081 updated.push_back(TypeAttr::get(newTy));
1082 changed = true;
1083 continue;
1084 }
1085 } else {
1086 Attribute converted = convertIfPossible(a);
1087 if (converted != a) {
1088 updated.push_back(converted);
1089 changed = true;
1090 continue;
1091 }
1092 }
1093 updated.push_back(a);
1094 }
1095 if (changed) {
1096 return getStructTypeWithParams(inputTy.getNameRef(), inputTy.getContext(), updated);
1097 }
1098 }
1099 return inputTy;
1100 });
1101 }
1102
1103 Attribute convertAttr(Attribute attr) const {
1104 if (TypeAttr tyAttr = llvm::dyn_cast<TypeAttr>(attr)) {
1105 Type convertedTy = convertType(tyAttr.getValue());
1106 if (convertedTy != tyAttr.getValue()) {
1107 return TypeAttr::get(convertedTy);
1108 }
1109 }
1110 return convertIfPossible(attr);
1111 }
1112
1113 bool containsParam(Attribute nameAttr) const { return paramNameToValue.contains(nameAttr); }
1114 const DenseMap<Attribute, Attribute> &getParamMap() const { return paramNameToValue; }
1115};
1116
1118inline static std::optional<Attribute>
1119inferUnifiedParam(const UnificationMap &unifyResult, SymbolRefAttr paramName) {
1120 auto it = unifyResult.find({paramName, Side::RHS});
1121 return (it == unifyResult.end()) ? std::nullopt : std::make_optional(it->second);
1122}
1123
1126inline static LogicalResult failIncompatibleInferredParam(
1127 CallOp op, PatternRewriter &rewriter, FlatSymbolRefAttr paramName, TemplateParamOp paramOp
1128) {
1129 LLVM_DEBUG(
1130 llvm::dbgs() << "[InstantiateFuncAtCallOp] unification for param '" << paramName
1131 << "': incompatible with specified param type. MUST FAIL!\n"
1132 );
1133 return rewriter.notifyMatchFailure(op, [&paramName, &paramOp](Diagnostic &diag) {
1134 diag.append("inferred value for parameter '")
1135 .append(paramName)
1136 .append("' is incompatible with specified param type")
1137 .attachNote(paramOp.getLoc())
1138 .append("template parameter declared here");
1139 });
1140}
1141
1144class WildcardTypeBodyInferer final {
1145 SymbolTableCollection &symTables_;
1146 const DenseMap<Attribute, Attribute> &paramNameToConcrete_;
1147 SmallVector<std::pair<Operation *, FlatSymbolRefAttr>> activeInferences_;
1148
1149public:
1150 WildcardTypeBodyInferer(
1151 SymbolTableCollection &symTables, const DenseMap<Attribute, Attribute> &paramNameToConcrete
1152 )
1153 : symTables_(symTables), paramNameToConcrete_(paramNameToConcrete) {}
1154
1155 std::optional<Attribute> infer(FuncDefOp func, FlatSymbolRefAttr paramName) {
1156 if (llvm::any_of(activeInferences_, [&](const auto &e) {
1157 return e.first == func.getOperation() && e.second == paramName;
1158 })) {
1159 return std::nullopt;
1160 }
1161 activeInferences_.emplace_back(func.getOperation(), paramName);
1162
1163 FuncInstTypeConverter tyConv((paramNameToConcrete_));
1164 std::optional<Attribute> inferred;
1165 bool ambiguous = false;
1166
1167 // Record a concrete candidate unless it conflicts with an earlier one, in which
1168 // case the wildcard is treated as ambiguous and left unresolved.
1169 auto noteCandidate = [&inferred, &ambiguous](Attribute candidate) {
1170 if (!candidate || !isConcreteAttr(candidate)) {
1171 return WalkResult::advance();
1172 }
1173 if (!inferred.has_value()) {
1174 inferred = candidate;
1175 return WalkResult::advance();
1176 }
1177 if (*inferred != candidate) {
1178 ambiguous = true;
1179 return WalkResult::interrupt();
1180 }
1181 return WalkResult::advance();
1182 };
1183
1184 WalkResult walkResult = func.walk([&](Operation *bodyOp) {
1185 if (auto castOp = llvm::dyn_cast<UnifiableCastOp>(bodyOp)) {
1186 Type inputTy = tyConv.convertType(castOp.getInput().getType());
1187 Type resultTy = tyConv.convertType(castOp.getResult().getType());
1188 if (auto inputTvar = llvm::dyn_cast<TypeVarType>(inputTy);
1189 inputTvar && inputTvar.getNameRef() == paramName && isConcreteType(resultTy)) {
1190 return noteCandidate(TypeAttr::get(resultTy));
1191 }
1192 if (auto resultTvar = llvm::dyn_cast<TypeVarType>(resultTy);
1193 resultTvar && resultTvar.getNameRef() == paramName && isConcreteType(inputTy)) {
1194 return noteCandidate(TypeAttr::get(inputTy));
1195 }
1196 return WalkResult::advance();
1197 }
1198
1199 auto nestedCall = llvm::dyn_cast<CallOp>(bodyOp);
1200 if (!nestedCall) {
1201 return WalkResult::advance();
1202 }
1203
1204 FailureOr<SymbolLookupResult<FuncDefOp>> nestedTgtOpt =
1205 nestedCall.getCalleeTarget(symTables_);
1206 if (failed(nestedTgtOpt)) {
1207 return WalkResult::advance();
1208 }
1209 FuncDefOp nestedTgt = nestedTgtOpt->get();
1210 auto nestedTemplate = llvm::dyn_cast<TemplateOp>(nestedTgt->getParentOp());
1211 if (!nestedTemplate) {
1212 return WalkResult::advance();
1213 }
1214
1215 TypeRange nestedResultTypes = nestedTgt.getFunctionType().getResults();
1216 for (auto [result, nestedResultTy] :
1217 llvm::zip_equal(nestedCall.getResults(), nestedResultTypes)) {
1218 Type convertedResultTy = tyConv.convertType(result.getType());
1219 auto resultTvar = llvm::dyn_cast<TypeVarType>(convertedResultTy);
1220 auto nestedTvar = llvm::dyn_cast<TypeVarType>(nestedResultTy);
1221 if (!resultTvar || !nestedTvar || resultTvar.getNameRef() != paramName) {
1222 continue;
1223 }
1224 if (std::optional<Attribute> candidate = inferFromExplicitNestedCallParams(
1225 nestedCall, nestedTemplate, nestedTvar.getNameRef(), tyConv
1226 )) {
1227 WalkResult candidateResult = noteCandidate(*candidate);
1228 if (candidateResult.wasInterrupted()) {
1229 return candidateResult;
1230 }
1231 continue;
1232 }
1233 if (std::optional<Attribute> candidate = infer(nestedTgt, nestedTvar.getNameRef())) {
1234 WalkResult candidateResult = noteCandidate(*candidate);
1235 if (candidateResult.wasInterrupted()) {
1236 return candidateResult;
1237 }
1238 }
1239 }
1240 return WalkResult::advance();
1241 });
1242
1243 activeInferences_.pop_back();
1244 if (ambiguous || (walkResult.wasInterrupted() && !inferred.has_value())) {
1245 return std::nullopt;
1246 }
1247 return inferred;
1248 }
1249
1250private:
1251 std::optional<Attribute> inferFromExplicitNestedCallParams(
1252 CallOp nestedCall, TemplateOp nestedTemplate, FlatSymbolRefAttr nestedParamName,
1253 const FuncInstTypeConverter &tyConv
1254 ) const {
1255 ArrayAttr nestedCallParams = nestedCall.getTemplateParamsAttr();
1256 if (isNullOrEmpty(nestedCallParams)) {
1257 return std::nullopt;
1258 }
1259
1260 for (auto [paramOp, attr] :
1261 llvm::zip_equal(nestedTemplate.getConstOps<TemplateParamOp>(), nestedCallParams)) {
1262 auto paramName = FlatSymbolRefAttr::get(paramOp.getSymNameAttr());
1263 if (paramName != nestedParamName) {
1264 continue;
1265 }
1266 Attribute convertedAttr = tyConv.convertAttr(attr);
1267 return isConcreteAttr(convertedAttr) ? std::make_optional(convertedAttr) : std::nullopt;
1268 }
1269 return std::nullopt;
1270 }
1271};
1272
1275class ClonedBodyArrayReadOpPattern final : public OpConversionPattern<ReadArrayOp> {
1276public:
1277 using OpConversionPattern<ReadArrayOp>::OpConversionPattern;
1278
1279 LogicalResult matchAndRewrite(
1280 ReadArrayOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter
1281 ) const override {
1282 Type newResultTy = getTypeConverter()->convertType(op.getResult().getType());
1283 if (!llvm::isa<ArrayType>(newResultTy)) {
1284 return failure();
1285 }
1287 rewriter, op, newResultTy, adaptor.getArrRef(), adaptor.getIndices()
1288 );
1289 return success();
1290 }
1291};
1292
1295class ClonedBodyArrayWriteOpPattern final : public OpConversionPattern<WriteArrayOp> {
1296public:
1297 using OpConversionPattern<WriteArrayOp>::OpConversionPattern;
1298
1299 LogicalResult matchAndRewrite(
1300 WriteArrayOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter
1301 ) const override {
1302 if (!llvm::isa<ArrayType>(adaptor.getRvalue().getType())) {
1303 return failure();
1304 }
1306 rewriter, op, adaptor.getArrRef(), adaptor.getIndices(), adaptor.getRvalue()
1307 );
1308 return success();
1309 }
1310};
1311
1315static LogicalResult applyBodyConversions(
1316 CallOp op, FuncDefOp newFunc, const DenseMap<Attribute, Attribute> &paramNameToConcrete
1317) {
1318 MLIRContext *ctx = op.getContext();
1319 FuncInstTypeConverter tyConv(paramNameToConcrete);
1320 ConversionTarget target = newConverterDefinedTarget<>(tyConv, ctx, tableOffsetIsntSymbol);
1321 target.addDynamicallyLegalOp<ConstReadOp>([&tyConv](ConstReadOp p) {
1322 // Legal if it's not in the map of concrete attribute instantiations
1323 return !tyConv.containsParam(p.getConstNameAttr());
1324 });
1325 SmallVector<Diagnostic> delayedDiagnostics;
1326 RewritePatternSet bodyPatterns = newGeneralRewritePatternSet(tyConv, ctx, target);
1327 bodyPatterns.add<ClonedBodyConstReadOpPattern>(
1328 tyConv, ctx, tyConv.getParamMap(), delayedDiagnostics
1329 );
1330 bodyPatterns.add<ClonedBodyArrayReadOpPattern, ClonedBodyArrayWriteOpPattern>(tyConv, ctx);
1331 bodyPatterns.add<ClonedMemberReadOpPattern>(tyConv, ctx, paramNameToConcrete);
1332 if (failed(applyFullConversion(newFunc, target, std::move(bodyPatterns)))) {
1333 return failure();
1334 }
1335 LLVM_DEBUG(llvm::dbgs() << "[InstantiateFuncAtCallOp] instantiated clone: " << newFunc << '\n');
1336 ::reportDelayedDiagnostics(op, std::move(delayedDiagnostics));
1337
1338 SymbolTableCollection tables;
1339 WalkResult res = newFunc.walk([&tables](CallOp nestedCall) {
1340 return WalkResult(nestedCall.verifySymbolUses(tables));
1341 });
1342 return failure(res.wasInterrupted());
1343}
1344
1345class InstantiateFuncAtCallOp final : public OpRewritePattern<CallOp> {
1346 ConversionTracker &tracker_;
1347
1348public:
1349 InstantiateFuncAtCallOp(MLIRContext *ctx, ConversionTracker &tracker)
1350 : OpRewritePattern<CallOp>(ctx), tracker_(tracker) {}
1351
1352 LogicalResult matchAndRewrite(CallOp op, PatternRewriter &rewriter) const override {
1353 LLVM_DEBUG(llvm::dbgs() << "[InstantiateFuncAtCallOp] op: " << op << '\n');
1354
1355 if (calleeReferencesTemplateParam(op)) {
1356 return failure();
1357 }
1358
1359 // Lookup callee target function
1360 SymbolTableCollection symTables;
1361 FailureOr<SymbolLookupResult<FuncDefOp>> callTgtOpt = op.getCalleeTarget(symTables);
1362 if (failed(callTgtOpt)) {
1363 return rewriter.notifyMatchFailure(op, [](Diagnostic &diag) {
1364 diag << "could not find target function for call";
1365 });
1366 }
1367 FuncDefOp callTgt = callTgtOpt->get();
1368
1369 // Check if callee is within a TemplateOp
1370 TemplateOp parentTemplate = llvm::dyn_cast<TemplateOp>(callTgt->getParentOp());
1371 if (!parentTemplate) {
1372 return failure(); // nothing to do if not parameterized
1373 }
1374 LLVM_DEBUG(
1375 llvm::dbgs() << "[InstantiateFuncAtCallOp] target function in template "
1376 << parentTemplate.getSymName() << '\n'
1377 );
1378
1379 // Perform type unification with tracking to infer the instantiated type(s). Even though
1380 // `CallOp` verification already checked that caller and callee types unify, the progress of
1381 // instantiation so far may have brought together a chain of calls across templates where each
1382 // individual unification check passed due to permissive type variables and/or symbols in the
1383 // middle but the overall chain does not unify. Hence, this unification may fail and should
1384 // produce a meaningful error message if it does.
1385 // See: `test/Transforms/Flattening/instantiate_funcs_fail.llzk`
1386 FailureOr<UnificationMap> unifyResult = unifyTypeSignature(op, callTgt, rewriter);
1387 if (failed(unifyResult)) {
1388 return failure();
1389 }
1390 LLVM_DEBUG(
1391 llvm::dbgs() << "[InstantiateFuncAtCallOp] unifications of types: "
1392 << debug::toStringList(unifyResult.value()) << '\n'
1393 );
1394
1395 // Maps template parameter symbols to the instantiation value at the call site.
1396 DenseMap<Attribute, Attribute> paramNameToConcrete;
1397 if (failed(collectConcreteTemplateParams(
1398 op, rewriter, symTables, callTgt, parentTemplate, unifyResult.value(),
1399 paramNameToConcrete
1400 ))) {
1401 return failure();
1402 }
1403
1404 if (paramNameToConcrete.empty()) {
1405 LLVM_DEBUG(llvm::dbgs() << "[InstantiateFuncAtCallOp] skip: no concrete params\n");
1406 return failure();
1407 }
1408
1409 evaluateTemplateExprs(parentTemplate, paramNameToConcrete);
1410
1411 FailureOr<InstantiationLayout> layoutResult =
1412 buildInstantiationLayout(parentTemplate, op.getTemplateParamsAttr(), paramNameToConcrete);
1413 if (failed(layoutResult)) {
1414 return failure();
1415 }
1416 InstantiationLayout layout = std::move(*layoutResult);
1417 ModuleOp parentModule = getParentOfType<ModuleOp>(parentTemplate);
1418 assert(parentModule && "TemplateOp must be nested in a ModuleOp");
1419
1420 SymbolRefAttr originalCalleeAttr = op.getCalleeAttr();
1421 FailureOr<SymbolRefAttr> newCalleeAttr =
1422 layout.remainingNames.empty()
1423 ? instantiateFully(
1424 op, rewriter, symTables, callTgt, parentTemplate, parentModule,
1425 layout.templateNameWithAttrs, paramNameToConcrete
1426 )
1427 : instantiatePartially(
1428 op, rewriter, symTables, callTgt, parentTemplate, parentModule, layout,
1429 paramNameToConcrete, tracker_
1430 );
1431 if (failed(newCalleeAttr)) {
1432 return failure();
1433 }
1434
1435 tracker_.recordInstantiation(originalCalleeAttr);
1436
1437 // Update the CallOp to point to the instantiated function and mark the module as modified.
1438 rewriter.modifyOpInPlace(op, [&op, &newCalleeAttr, &layout]() {
1439 LLVM_DEBUG({
1440 llvm::dbgs() << "[InstantiateFuncAtCallOp] updating callee from " << op.getCalleeAttr()
1441 << " to " << *newCalleeAttr << '\n';
1442 });
1443 op.setCalleeAttr(*newCalleeAttr);
1445 });
1446 tracker_.updateModifiedFlag(true);
1447 return success();
1448 }
1449
1450private:
1453 static FailureOr<UnificationMap>
1454 unifyTypeSignature(CallOp op, FuncDefOp callTgt, PatternRewriter &rewriter) {
1455 FailureOr<UnificationMap> unifyResult = op.unifyTypeSignature(callTgt.getFunctionType());
1456 if (succeeded(unifyResult)) {
1457 return unifyResult;
1458 }
1459 return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) {
1460 diag.append("target function type does not unify with call type ")
1461 .append(op.getTypeSignature())
1462 .attachNote(callTgt.getLoc())
1463 .append("target function declared here");
1464 });
1465 }
1466
1469 static LogicalResult collectConcreteTemplateParams(
1470 CallOp op, PatternRewriter &rewriter, SymbolTableCollection &symTables, FuncDefOp callTgt,
1471 TemplateOp parentTemplate, const UnificationMap &unifyResult,
1472 DenseMap<Attribute, Attribute> &paramNameToConcrete
1473 ) {
1474 auto realParams = parentTemplate.getConstOps<TemplateParamOp>();
1475 ArrayAttr callParams = op.getTemplateParamsAttr();
1476 LLVM_DEBUG(
1477 llvm::dbgs() << "[InstantiateFuncAtCallOp] TemplateParamsAttr: " << callParams << '\n'
1478 );
1479
1480 auto recordConcreteParam = [&](FlatSymbolRefAttr paramName, TemplateParamOp paramOp,
1481 Attribute concreteValue) {
1482 if (failed(op.verifyTemplateParamCompatibility(concreteValue, paramOp))) {
1483 return failIncompatibleInferredParam(op, rewriter, paramName, paramOp);
1484 }
1485 paramNameToConcrete[paramName] = concreteValue;
1486 return success();
1487 };
1488
1489 // If there's no template instantiation list, must infer all template parameters.
1490 if (isNullOrEmpty(callParams)) {
1491 for (auto paramOp : realParams) {
1492 auto paramName = FlatSymbolRefAttr::get(paramOp.getSymNameAttr());
1493 auto inferredValOpt = inferUnifiedParam(unifyResult, paramName);
1494 if (!inferredValOpt.has_value()) {
1495 LLVM_DEBUG(
1496 llvm::dbgs() << "[InstantiateFuncAtCallOp] unification for param '" << paramName
1497 << "': not found\n"
1498 );
1499 continue;
1500 }
1501 Attribute inferredVal = *inferredValOpt;
1502 LLVM_DEBUG(
1503 llvm::dbgs() << "[InstantiateFuncAtCallOp] inferredVal: " << inferredVal << '\n'
1504 );
1505 if (!isConcreteAttr(inferredVal)) {
1506 LLVM_DEBUG(
1507 llvm::dbgs() << "[InstantiateFuncAtCallOp] unification for param '" << paramName
1508 << "': not concrete, " << inferredVal << '\n'
1509 );
1510 continue;
1511 }
1512 if (failed(recordConcreteParam(paramName, paramOp, inferredVal))) {
1513 return failure();
1514 }
1515 }
1516 return success();
1517 }
1518
1519 // As stated earlier, need to run the verification checks again to ensure the
1520 // instantiation is valid, except for the size check because that cannot change.
1521 assert((callParams.size() == llvm::range_size(realParams)) && "per CallOpVerifier");
1522 if (failed(op.verifyTemplateParamCompatibility(realParams))) {
1523 return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) {
1524 diag.append("incompatible with specified param type(s)");
1525 });
1526 }
1527 if (failed(op.verifyTemplateParamsMatchInferred(realParams, unifyResult))) {
1528 return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) {
1529 diag.append("incompatible with inferred param value(s)");
1530 });
1531 }
1532
1533 // When template parameters are specified on the CallOp, use them as the source of truth
1534 // for concrete arguments, then infer wildcard parameters against the full explicit map.
1535 SmallVector<std::pair<TemplateParamOp, FlatSymbolRefAttr>> wildcardParams;
1536 for (auto [paramOp, attr] : llvm::zip_equal(realParams, callParams.getValue())) {
1537 auto paramName = FlatSymbolRefAttr::get(paramOp.getSymNameAttr());
1538 AttrConcreteness classification = classifyAttrConcreteness(attr);
1539 if (classification == AttrConcreteness::Concrete) {
1540 paramNameToConcrete[paramName] = attr;
1541 continue;
1542 }
1543
1544 if (classification == AttrConcreteness::NonConcrete) {
1545 LLVM_DEBUG(
1546 llvm::dbgs() << "[InstantiateFuncAtCallOp] unification for param '" << paramName
1547 << "': not concrete, " << attr << '\n'
1548 );
1549 continue;
1550 }
1551 wildcardParams.emplace_back(paramOp, paramName);
1552 }
1553
1554 WildcardTypeBodyInferer bodyInferer(symTables, paramNameToConcrete);
1555 for (auto [paramOp, paramName] : wildcardParams) {
1556 auto inferredValOpt = inferUnifiedParam(unifyResult, paramName);
1557 if (inferredValOpt.has_value() && isConcreteAttr(*inferredValOpt)) {
1558 LLVM_DEBUG(
1559 llvm::dbgs() << "[InstantiateFuncAtCallOp] inferredVal: " << *inferredValOpt << '\n'
1560 );
1561 if (failed(recordConcreteParam(paramName, paramOp, *inferredValOpt))) {
1562 return failure();
1563 }
1564 continue;
1565 }
1566
1567 inferredValOpt = bodyInferer.infer(callTgt, paramName);
1568 if (inferredValOpt.has_value() && isConcreteAttr(*inferredValOpt)) {
1569 LLVM_DEBUG(
1570 llvm::dbgs() << "[InstantiateFuncAtCallOp] body-inferred value for param '"
1571 << paramName << "': " << *inferredValOpt << '\n'
1572 );
1573 if (failed(recordConcreteParam(paramName, paramOp, *inferredValOpt))) {
1574 return failure();
1575 }
1576 }
1577 }
1578 return success();
1579 }
1580
1583 static FailureOr<SymbolRefAttr> instantiateFully(
1584 CallOp op, PatternRewriter &rewriter, SymbolTableCollection &symTables, FuncDefOp callTgt,
1585 TemplateOp parentTemplate, ModuleOp parentModule, StringRef templateNameWithAttrs,
1586 const DenseMap<Attribute, Attribute> &paramNameToConcrete
1587 ) {
1588 MLIRContext *ctx = op.getContext();
1589 std::string newFuncName =
1590 (mlir::Twine(templateNameWithAttrs) + "_" + callTgt.getSymName()).str();
1591 StringRef actualNewFuncName = newFuncName;
1592 if (!symTables.getSymbolTable(parentModule).lookup(newFuncName)) {
1593 FuncDefOp newFunc = callTgt.clone();
1594 newFunc.setSymName(newFuncName);
1595 convertCalleesInPlace(newFunc, paramNameToConcrete);
1596 // Insert before the TemplateOp; symbol table may adjust the name to ensure uniqueness.
1597 symTables.getSymbolTable(parentModule).insert(newFunc, Block::iterator(parentTemplate));
1598 actualNewFuncName = newFunc.getSymName();
1599 LLVM_DEBUG(
1600 llvm::dbgs() << "[InstantiateFuncAtCallOp] created full instantiation function: "
1601 << actualNewFuncName << '\n'
1602 );
1603 if (failed(applyBodyConversions(op, newFunc, paramNameToConcrete))) {
1604 LLVM_DEBUG(
1605 llvm::dbgs() << "[InstantiateFuncAtCallOp] body conversion failed for "
1606 << actualNewFuncName << '\n'
1607 );
1608 newFunc->erase();
1609 return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) {
1610 diag.append("failure while creating instantiated function '", actualNewFuncName, '\'');
1611 });
1612 }
1613 } else {
1614 LLVM_DEBUG(
1615 llvm::dbgs() << "[InstantiateFuncAtCallOp] reusing full instantiation function: "
1616 << actualNewFuncName << '\n'
1617 );
1618 }
1619
1620 // Callee: drop template & original function names, add the new module-level function name.
1621 // Original: @[prefix...]::@TemplateName::@funcName
1622 // New: @[prefix...]::@newFuncName
1623 SmallVector<FlatSymbolRefAttr> symPieces = getPieces(op.getCalleeAttr());
1624 assert(symPieces.size() >= 2 && "callee must include at least template and function names");
1625 symPieces.pop_back(); // remove original function name
1626 symPieces.pop_back(); // remove template name
1627 symPieces.push_back(FlatSymbolRefAttr::get(StringAttr::get(ctx, actualNewFuncName)));
1628 return asSymbolRefAttr(symPieces);
1629 }
1630
1635 static FailureOr<SymbolRefAttr> instantiatePartially(
1636 CallOp op, PatternRewriter &rewriter, SymbolTableCollection &symTables, FuncDefOp callTgt,
1637 TemplateOp parentTemplate, ModuleOp parentModule, const InstantiationLayout &layout,
1638 const DenseMap<Attribute, Attribute> &paramNameToConcrete, ConversionTracker &tracker
1639 ) {
1640 if (auto cached = tracker.lookupPartialFuncInstantiation(callTgt, layout.concreteParamKey)) {
1641 SmallVector<FlatSymbolRefAttr> symPieces = getPieces(op.getCalleeAttr());
1642 SmallVector<FlatSymbolRefAttr> cachedSuffix = getPieces(*cached);
1643 assert(symPieces.size() >= 2 && "callee must include at least template and function names");
1644 assert(cachedSuffix.size() == 2 && "cached callee suffix must contain template and function");
1645 symPieces.pop_back();
1646 symPieces.pop_back();
1647 symPieces.push_back(cachedSuffix[0]);
1648 symPieces.push_back(cachedSuffix[1]);
1649 SymbolRefAttr cachedCallee = asSymbolRefAttr(symPieces);
1650 LLVM_DEBUG(
1651 llvm::dbgs() << "[InstantiateFuncAtCallOp] reusing partial instantiation: "
1652 << cachedCallee << '\n'
1653 );
1654 return cachedCallee;
1655 }
1656 TemplateOp newTemplate = parentTemplate.cloneWithoutRegions();
1657 newTemplate.setSymName(layout.templateNameWithAttrs);
1658 setInstantiationNamePattern(newTemplate, layout.namePattern);
1659 assert(newTemplate->getNumRegions() > 0 && "region exists");
1660 newTemplate.getBodyRegion().emplaceBlock();
1661
1662 Block &newTemplateBody = newTemplate.getBodyRegion().front();
1663 for (Attribute name : layout.remainingNames) {
1664 FlatSymbolRefAttr nameSym = llvm::cast<FlatSymbolRefAttr>(name);
1665 Operation *paramOp = symTables.getSymbolTable(parentTemplate).lookup(nameSym.getAttr());
1666 assert(paramOp && "symbol must exist");
1667 newTemplateBody.push_back(paramOp->clone());
1668 }
1669
1670 // Clone and partially convert the function (concretize only the concrete params).
1671 FuncDefOp newFunc = callTgt.clone();
1672 convertCalleesInPlace(newFunc, paramNameToConcrete);
1673
1674 // Insert before body conversion so nested concrete callees verify from the root module. Use
1675 // SymbolTable::insert() so both physical symbol names are unique if necessary.
1676 symTables.getSymbolTable(newTemplate).insert(newFunc);
1677 symTables.getSymbolTable(parentModule).insert(newTemplate, Block::iterator(parentTemplate));
1678 if (failed(applyBodyConversions(op, newFunc, paramNameToConcrete))) {
1679 std::string newFuncName = newFunc.getSymName().str();
1680 LLVM_DEBUG(
1681 llvm::dbgs() << "[InstantiateFuncAtCallOp] body conversion failed for " << newFuncName
1682 << '\n'
1683 );
1684 newTemplate->erase();
1685 return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) {
1686 diag.append("failure while creating instantiated function '", newFuncName, '\'');
1687 });
1688 }
1689
1690 // Use the post-insertion names. The preferred template name may have collided.
1691 SmallVector<FlatSymbolRefAttr> symPieces = getPieces(op.getCalleeAttr());
1692 assert(symPieces.size() >= 2 && "callee must include at least template and function names");
1693 symPieces.pop_back();
1694 symPieces.pop_back(); // remove original template name
1695 symPieces.push_back(FlatSymbolRefAttr::get(newTemplate.getSymNameAttr()));
1696 symPieces.push_back(FlatSymbolRefAttr::get(newFunc.getSymNameAttr()));
1697 SymbolRefAttr newCallee = asSymbolRefAttr(symPieces);
1698
1699 LLVM_DEBUG(
1700 llvm::dbgs() << "[InstantiateFuncAtCallOp] created partial instantiation: " << newCallee
1701 << '\n'
1702 );
1703 // Publish only after insertion and body conversion have succeeded.
1704 tracker.recordPartialFuncInstantiation(callTgt, layout.concreteParamKey, newTemplate, newFunc);
1705 return newCallee;
1706 }
1707};
1708
1709LogicalResult run(ModuleOp modOp, ConversionTracker &tracker) {
1710 MLIRContext *ctx = modOp.getContext();
1711 RewritePatternSet patterns(ctx);
1712 patterns.add<InstantiateFuncAtCallOp>(ctx, tracker);
1713 MatchFailureListener failureListener;
1714 walkAndApplyPatterns(modOp, std::move(patterns), &failureListener);
1715 return failure(failureListener.hadFailure);
1716}
1717
1718} // namespace Step2_InstantiateFunctions
1719
1720namespace Step3_Unroll {
1721
1722// TODO: not guaranteed to work with WhileOp, can try with our custom attributes though.
1723template <HasInterface<LoopLikeOpInterface> OpClass>
1724class LoopUnrollPattern : public OpRewritePattern<OpClass> {
1725public:
1726 using OpRewritePattern<OpClass>::OpRewritePattern;
1727
1728 LogicalResult matchAndRewrite(OpClass loopOp, PatternRewriter &rewriter) const override {
1729 if (auto maybeConstant = getConstantTripCount(loopOp)) {
1730 uint64_t tripCount = *maybeConstant;
1731 if (tripCount == 0) {
1732 rewriter.eraseOp(loopOp);
1733 return success();
1734 } else if (tripCount == 1) {
1735 return loopOp.promoteIfSingleIteration(rewriter);
1736 }
1737 return loopUnrollByFactor(loopOp, tripCount);
1738 }
1739 return failure();
1740 }
1741
1742private:
1745 static std::optional<int64_t> getConstantTripCount(LoopLikeOpInterface loopOp) {
1746 std::optional<OpFoldResult> lbVal = loopOp.getSingleLowerBound();
1747 std::optional<OpFoldResult> ubVal = loopOp.getSingleUpperBound();
1748 std::optional<OpFoldResult> stepVal = loopOp.getSingleStep();
1749 if (!lbVal.has_value() || !ubVal.has_value() || !stepVal.has_value()) {
1750 return std::nullopt;
1751 }
1752 return constantTripCount(lbVal.value(), ubVal.value(), stepVal.value());
1753 }
1754};
1755
1756LogicalResult run(ModuleOp modOp, ConversionTracker &tracker) {
1757 MLIRContext *ctx = modOp.getContext();
1758 RewritePatternSet patterns(ctx);
1759 patterns.add<LoopUnrollPattern<scf::ForOp>>(ctx);
1760 patterns.add<LoopUnrollPattern<affine::AffineForOp>>(ctx);
1761
1762 return applyAndFoldGreedily(modOp, tracker, std::move(patterns));
1763}
1764} // namespace Step3_Unroll
1765
1767
1768// Adapted from `mlir::getConstantIntValues()` but that one failed in CI for an unknown reason. This
1769// version uses a basic loop instead of llvm::map_to_vector().
1770std::optional<SmallVector<int64_t>> getConstantIntValues(ArrayRef<OpFoldResult> ofrs) {
1771 SmallVector<int64_t> res;
1772 for (OpFoldResult ofr : ofrs) {
1773 std::optional<int64_t> cv = getConstantIntValue(ofr);
1774 if (!cv.has_value()) {
1775 return std::nullopt;
1776 }
1777 res.push_back(cv.value());
1778 }
1779 return res;
1780}
1781
1782struct AffineMapFolder {
1783 struct Input {
1784 OperandRangeRange mapOpGroups;
1785 DenseI32ArrayAttr dimsPerGroup;
1786 ArrayRef<Attribute> paramsOfStructTy;
1787 };
1788
1789 struct Output {
1790 SmallVector<SmallVector<Value>> mapOpGroups;
1791 SmallVector<int32_t> dimsPerGroup;
1792 SmallVector<Attribute> paramsOfStructTy;
1793 };
1794
1795 static inline SmallVector<ValueRange> getConvertedMapOpGroups(Output out) {
1796 return llvm::map_to_vector(out.mapOpGroups, [](const SmallVector<Value> &grp) {
1797 return ValueRange(grp);
1798 });
1799 }
1800
1801 static LogicalResult
1802 fold(PatternRewriter &rewriter, const Input &in, Output &out, Operation *op, const char *aspect) {
1803 if (in.mapOpGroups.empty()) {
1804 // No affine map operands so nothing to do
1805 return failure();
1806 }
1807
1808 assert(in.mapOpGroups.size() <= in.paramsOfStructTy.size());
1809 assert(std::cmp_equal(in.mapOpGroups.size(), in.dimsPerGroup.size()));
1810
1811 size_t idx = 0; // index in `mapOpGroups`, i.e., the number of AffineMapAttr encountered
1812 for (Attribute sizeAttr : in.paramsOfStructTy) {
1813 if (AffineMapAttr m = dyn_cast<AffineMapAttr>(sizeAttr)) {
1814 ValueRange currMapOps = in.mapOpGroups[idx++];
1815 LLVM_DEBUG(
1816 llvm::dbgs() << "[AffineMapFolder] currMapOps: " << debug::toStringList(currMapOps)
1817 << '\n'
1818 );
1819 SmallVector<OpFoldResult> currMapOpsCast = getAsOpFoldResult(currMapOps);
1820 LLVM_DEBUG(
1821 llvm::dbgs() << "[AffineMapFolder] currMapOps as fold results: "
1822 << debug::toStringList(currMapOpsCast) << '\n'
1823 );
1824 if (auto constOps = Step4_InstantiateAffineMaps::getConstantIntValues(currMapOpsCast)) {
1825 SmallVector<Attribute> result;
1826 bool hasPoison = false; // indicates divide by 0 or mod by <1
1827 auto constAttrs = llvm::map_to_vector(*constOps, [&rewriter](int64_t v) -> Attribute {
1828 return rewriter.getIndexAttr(v);
1829 });
1830 LogicalResult foldResult = m.getAffineMap().constantFold(constAttrs, result, &hasPoison);
1831 if (hasPoison) {
1832 // Diagnostic remark: could be removed for release builds if too noisy
1833 op->emitRemark()
1834 .append(
1835 "Cannot fold affine_map for ", aspect, ' ', out.paramsOfStructTy.size(),
1836 " due to divide by 0 or modulus with negative divisor"
1837 )
1838 .report();
1839 return failure();
1840 }
1841 if (failed(foldResult)) {
1842 // Diagnostic remark: could be removed for release builds if too noisy
1843 op->emitRemark()
1844 .append(
1845 "Folding affine_map for ", aspect, ' ', out.paramsOfStructTy.size(), " failed"
1846 )
1847 .report();
1848 return failure();
1849 }
1850 if (result.size() != 1) {
1851 // Diagnostic remark: could be removed for release builds if too noisy
1852 op->emitRemark()
1853 .append(
1854 "Folding affine_map for ", aspect, ' ', out.paramsOfStructTy.size(),
1855 " produced ", result.size(), " results but expected 1"
1856 )
1857 .report();
1858 return failure();
1859 }
1860 assert(!llvm::isa<AffineMapAttr>(result[0]) && "not converted");
1861 out.paramsOfStructTy.push_back(result[0]);
1862 continue;
1863 }
1864 // If affine but not foldable, preserve the map ops
1865 out.mapOpGroups.emplace_back(currMapOps);
1866 out.dimsPerGroup.push_back(in.dimsPerGroup[idx - 1]); // idx was already incremented
1867 }
1868 // If not affine and foldable, preserve the original
1869 out.paramsOfStructTy.push_back(sizeAttr);
1870 }
1871 assert(idx == in.mapOpGroups.size() && "all affine_map not processed");
1872 assert(
1873 in.paramsOfStructTy.size() == out.paramsOfStructTy.size() &&
1874 "produced wrong number of dimensions"
1875 );
1876
1877 return success();
1878 }
1879};
1880
1882class InstantiateAtCreateArrayOp final : public OpRewritePattern<CreateArrayOp> {
1883 [[maybe_unused]]
1884 ConversionTracker &tracker_;
1885
1886public:
1887 InstantiateAtCreateArrayOp(MLIRContext *ctx, ConversionTracker &tracker)
1888 : OpRewritePattern(ctx), tracker_(tracker) {}
1889
1890 LogicalResult matchAndRewrite(CreateArrayOp op, PatternRewriter &rewriter) const override {
1891 ArrayType oldResultType = op.getType();
1892
1893 AffineMapFolder::Output out;
1894 AffineMapFolder::Input in = {
1895 op.getMapOperands(),
1897 oldResultType.getDimensionSizes(),
1898 };
1899 if (failed(AffineMapFolder::fold(rewriter, in, out, op, "array dimension"))) {
1900 return failure();
1901 }
1902
1903 ArrayType newResultType = ArrayType::get(oldResultType.getElementType(), out.paramsOfStructTy);
1904 if (newResultType == oldResultType) {
1905 return failure(); // nothing changed
1906 }
1907 // ASSERT: folding only preserves the original Attribute or converts affine to integer
1908 assert(tracker_.isLegalConversion(oldResultType, newResultType, "InstantiateAtCreateArrayOp"));
1909 LLVM_DEBUG(
1910 llvm::dbgs() << "[InstantiateAtCreateArrayOp] instantiating " << oldResultType << " as "
1911 << newResultType << " in \"" << op << "\"\n"
1912 );
1914 rewriter, op, newResultType, AffineMapFolder::getConvertedMapOpGroups(out), out.dimsPerGroup
1915 );
1916 return success();
1917 }
1918};
1919
1921class InstantiateAtCallOpCompute final : public OpRewritePattern<CallOp> {
1922 ConversionTracker &tracker_;
1923
1924public:
1925 InstantiateAtCallOpCompute(MLIRContext *ctx, ConversionTracker &tracker)
1926 : OpRewritePattern(ctx), tracker_(tracker) {}
1927
1928 LogicalResult matchAndRewrite(CallOp op, PatternRewriter &rewriter) const override {
1929 if (!op.calleeIsStructCompute()) {
1930 // this pattern only applies when the callee is "compute()" within a struct
1931 return failure();
1932 }
1933 LLVM_DEBUG(llvm::dbgs() << "[InstantiateAtCallOpCompute] target: " << op.getCallee() << '\n');
1935 LLVM_DEBUG(llvm::dbgs() << "[InstantiateAtCallOpCompute] oldRetTy: " << oldRetTy << '\n');
1936 ArrayAttr params = oldRetTy.getParams();
1937 if (isNullOrEmpty(params)) {
1938 // nothing to do if the StructType is not parameterized
1939 return failure();
1940 }
1941
1942 AffineMapFolder::Output out;
1943 AffineMapFolder::Input in = {
1944 op.getMapOperands(),
1946 params.getValue(),
1947 };
1948 if (!in.mapOpGroups.empty()) {
1949 // If there are affine map operands, attempt to fold them to a constant.
1950 if (failed(AffineMapFolder::fold(rewriter, in, out, op, "struct parameter"))) {
1951 return failure();
1952 }
1953 LLVM_DEBUG({
1954 llvm::dbgs() << "[InstantiateAtCallOpCompute] folded affine_map in result type params\n";
1955 });
1956 } else {
1957 // If there are no affine map operands, attempt to refine the result type of the CallOp using
1958 // the function argument types and the type of the target function.
1959 auto callArgTypes = op.getArgOperands().getTypes();
1960 if (callArgTypes.empty()) {
1961 // no refinement possible if no function arguments
1962 return failure();
1963 }
1964 if (calleeReferencesTemplateParam(op)) {
1965 return failure();
1966 }
1967 SymbolTableCollection tables;
1968 auto lookupRes = lookupTopLevelSymbol<FuncDefOp>(tables, op.getCalleeAttr(), op);
1969 if (failed(lookupRes)) {
1970 return failure();
1971 }
1972 if (failed(instantiateViaTargetType(in, out, callArgTypes, lookupRes->get()))) {
1973 return failure();
1974 }
1975 LLVM_DEBUG({
1976 llvm::dbgs() << "[InstantiateAtCallOpCompute] propagated instantiations via symrefs in "
1977 "result type params: "
1978 << debug::toStringList(out.paramsOfStructTy) << '\n';
1979 });
1980 }
1981
1982 StructType newRetTy = StructType::get(oldRetTy.getNameRef(), out.paramsOfStructTy);
1983 LLVM_DEBUG(llvm::dbgs() << "[InstantiateAtCallOpCompute] newRetTy: " << newRetTy << '\n');
1984 if (newRetTy == oldRetTy) {
1985 return failure(); // nothing changed
1986 }
1987 // The `newRetTy` is computed via instantiateViaTargetType() which can only preserve the
1988 // original Attribute or convert to a concrete attribute via the unification process. Thus, if
1989 // the conversion here is illegal it means there is a type conflict within the LLZK code that
1990 // prevents instantiation of the struct with the requested type.
1991 if (!tracker_.isLegalConversion(oldRetTy, newRetTy, "InstantiateAtCallOpCompute")) {
1992 return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) {
1993 diag.append(
1994 "result type mismatch: due to struct instantiation, expected type ", newRetTy,
1995 ", but found ", oldRetTy
1996 );
1997 });
1998 }
1999 LLVM_DEBUG(llvm::dbgs() << "[InstantiateAtCallOpCompute] replaced " << op);
2001 rewriter, op, TypeRange {newRetTy}, op.getCallee(),
2002 AffineMapFolder::getConvertedMapOpGroups(out), out.dimsPerGroup, op.getArgOperands()
2003 );
2004 (void)newOp; // tell compiler it's intentionally unused in release builds
2005 LLVM_DEBUG(llvm::dbgs() << " with " << newOp << '\n');
2006 return success();
2007 }
2008
2009private:
2012 inline LogicalResult instantiateViaTargetType(
2013 const AffineMapFolder::Input &in, AffineMapFolder::Output &out,
2014 OperandRange::type_range callArgTypes, FuncDefOp targetFunc
2015 ) const {
2016 assert(targetFunc.isStructCompute()); // since `op.calleeIsStructCompute()`
2017 ArrayAttr targetResTyParams = targetFunc.getSingleResultTypeOfCompute().getParams();
2018 assert(!isNullOrEmpty(targetResTyParams)); // same cardinality as `in.paramsOfStructTy`
2019 assert(in.paramsOfStructTy.size() == targetResTyParams.size()); // verifier ensures this
2020
2021 if (llvm::all_of(in.paramsOfStructTy, isConcreteAttr<>)) {
2022 // Nothing can change if everything is already concrete
2023 return failure();
2024 }
2025
2026 LLVM_DEBUG({
2027 llvm::dbgs() << '[' << __FUNCTION__ << ']'
2028 << " call arg types: " << debug::toStringList(callArgTypes) << '\n';
2029 llvm::dbgs() << '[' << __FUNCTION__ << ']' << " target func arg types: "
2030 << debug::toStringList(targetFunc.getArgumentTypes()) << '\n';
2031 llvm::dbgs() << '[' << __FUNCTION__ << ']'
2032 << " struct params @ call: " << debug::toStringList(in.paramsOfStructTy) << '\n';
2033 llvm::dbgs() << '[' << __FUNCTION__ << ']'
2034 << " target struct params: " << debug::toStringList(targetResTyParams) << '\n';
2035 });
2036
2037 UnificationMap unifications;
2038 bool unifies = typeListsUnify(targetFunc.getArgumentTypes(), callArgTypes, {}, &unifications);
2039 (void)unifies; // tell compiler it's intentionally unused in builds without assertions
2040 assert(unifies && "should have been checked by verifiers");
2041
2042 LLVM_DEBUG({
2043 llvm::dbgs() << '[' << __FUNCTION__ << ']'
2044 << " unifications of arg types: " << debug::toStringList(unifications) << '\n';
2045 });
2046
2047 // Check for LHS SymRef (i.e., from the target function) that have RHS concrete Attributes (i.e.
2048 // from the call argument types) without any struct parameters (because the type with concrete
2049 // struct parameters will be used to instantiate the target struct rather than the fully
2050 // flattened struct type resulting in type mismatch of the callee to target) and perform those
2051 // replacements in the `targetFunc` return type to produce the new result type for the CallOp.
2052 SmallVector<Attribute> newReturnStructParams = llvm::map_to_vector(
2053 llvm::zip_equal(targetResTyParams.getValue(), in.paramsOfStructTy),
2054 [&unifications](std::tuple<Attribute, Attribute> p) {
2055 Attribute fromCall = std::get<1>(p);
2056 // Preserve attributes that are already concrete at the call site. Otherwise attempt to lookup
2057 // non-parameterized concrete unification for the target struct parameter symbol.
2058 if (!isConcreteAttr(fromCall)) {
2059 Attribute fromTgt = std::get<0>(p);
2060 LLVM_DEBUG({
2061 llvm::dbgs() << "[instantiateViaTargetType] fromCall = " << fromCall << '\n';
2062 llvm::dbgs() << "[instantiateViaTargetType] fromTgt = " << fromTgt << '\n';
2063 });
2064 assert(llvm::isa<SymbolRefAttr>(fromTgt));
2065 auto it = unifications.find(std::make_pair(llvm::cast<SymbolRefAttr>(fromTgt), Side::LHS));
2066 if (it != unifications.end()) {
2067 Attribute unifiedAttr = it->second;
2068 LLVM_DEBUG({
2069 llvm::dbgs() << "[instantiateViaTargetType] unifiedAttr = " << unifiedAttr << '\n';
2070 });
2071 if (unifiedAttr && isConcreteAttr<false>(unifiedAttr)) {
2072 return unifiedAttr;
2073 }
2074 }
2075 }
2076 return fromCall;
2077 }
2078 );
2079
2080 out.paramsOfStructTy = newReturnStructParams;
2081 assert(out.paramsOfStructTy.size() == in.paramsOfStructTy.size() && "post-condition");
2082 assert(out.mapOpGroups.empty() && "post-condition");
2083 assert(out.dimsPerGroup.empty() && "post-condition");
2084 return success();
2085 }
2086};
2087
2088LogicalResult run(ModuleOp modOp, ConversionTracker &tracker) {
2089 MLIRContext *ctx = modOp.getContext();
2090 RewritePatternSet patterns(ctx);
2091 patterns.add<
2092 InstantiateAtCreateArrayOp, // CreateArrayOp
2093 InstantiateAtCallOpCompute // CallOp, targeting struct "compute()"
2094 >(ctx, tracker);
2095
2096 return applyAndFoldGreedily(modOp, tracker, std::move(patterns));
2097}
2098
2099} // namespace Step4_InstantiateAffineMaps
2100
2102
2104class UpdateNewArrayElemFromWrite final : public OpRewritePattern<CreateArrayOp> {
2105 ConversionTracker &tracker_;
2106
2107public:
2108 UpdateNewArrayElemFromWrite(MLIRContext *ctx, ConversionTracker &tracker)
2109 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
2110
2111 LogicalResult matchAndRewrite(CreateArrayOp op, PatternRewriter &rewriter) const override {
2112 Value createResult = op.getResult();
2113 ArrayType createResultType = dyn_cast<ArrayType>(createResult.getType());
2114 assert(createResultType && "CreateArrayOp must produce ArrayType");
2115 Type oldResultElemType = createResultType.getElementType();
2116
2117 // Look for WriteArrayOp where the array reference is the result of the CreateArrayOp and the
2118 // element type is different.
2119 Type newResultElemType = nullptr;
2120 for (Operation *user : createResult.getUsers()) {
2121 if (WriteArrayOp writeOp = dyn_cast<WriteArrayOp>(user)) {
2122 if (writeOp.getArrRef() != createResult) {
2123 continue;
2124 }
2125 Type writeRValueType = writeOp.getRvalue().getType();
2126 if (writeRValueType == oldResultElemType) {
2127 continue;
2128 }
2129 if (newResultElemType && newResultElemType != writeRValueType) {
2130 LLVM_DEBUG(
2131 llvm::dbgs()
2132 << "[UpdateNewArrayElemFromWrite] multiple possible element types for CreateArrayOp "
2133 << newResultElemType << " vs " << writeRValueType << '\n'
2134 );
2135 return failure();
2136 }
2137 newResultElemType = writeRValueType;
2138 }
2139 }
2140 if (!newResultElemType) {
2141 // no replacement type found
2142 return failure();
2143 }
2144 if (!tracker_.isLegalConversion(
2145 oldResultElemType, newResultElemType, "UpdateNewArrayElemFromWrite"
2146 )) {
2147 return failure();
2148 }
2149 ArrayType newType = createResultType.cloneWith(newResultElemType);
2150 rewriter.modifyOpInPlace(op, [&createResult, &newType]() { createResult.setType(newType); });
2151 LLVM_DEBUG(
2152 llvm::dbgs() << "[UpdateNewArrayElemFromWrite] updated result type of " << op << '\n'
2153 );
2154 return success();
2155 }
2156};
2157
2158namespace {
2159
2160LogicalResult updateArrayElemFromArrAccessOp(
2161 ArrayAccessOpInterface op, Type scalarElemTy, ConversionTracker &tracker,
2162 PatternRewriter &rewriter
2163) {
2164 ArrayType oldArrType = op.getArrRefType();
2165 if (oldArrType.getElementType() == scalarElemTy) {
2166 return failure(); // no change needed
2167 }
2168 ArrayType newArrType = oldArrType.cloneWith(scalarElemTy);
2169 if (oldArrType == newArrType ||
2170 !tracker.isLegalConversion(oldArrType, newArrType, "updateArrayElemFromArrAccessOp")) {
2171 return failure();
2172 }
2173 rewriter.modifyOpInPlace(op, [&op, &newArrType]() { op.getArrRef().setType(newArrType); });
2174 LLVM_DEBUG(
2175 llvm::dbgs() << "[updateArrayElemFromArrAccessOp] updated base array type in " << op << '\n'
2176 );
2177 return success();
2178}
2179
2180} // namespace
2181
2182class UpdateArrayElemFromArrWrite final : public OpRewritePattern<WriteArrayOp> {
2183 ConversionTracker &tracker_;
2184
2185public:
2186 UpdateArrayElemFromArrWrite(MLIRContext *ctx, ConversionTracker &tracker)
2187 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
2188
2189 LogicalResult matchAndRewrite(WriteArrayOp op, PatternRewriter &rewriter) const override {
2190 return updateArrayElemFromArrAccessOp(op, op.getRvalue().getType(), tracker_, rewriter);
2191 }
2192};
2193
2194class UpdateArrayElemFromArrRead final : public OpRewritePattern<ReadArrayOp> {
2195 ConversionTracker &tracker_;
2196
2197public:
2198 UpdateArrayElemFromArrRead(MLIRContext *ctx, ConversionTracker &tracker)
2199 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
2200
2201 LogicalResult matchAndRewrite(ReadArrayOp op, PatternRewriter &rewriter) const override {
2202 return updateArrayElemFromArrAccessOp(op, op.getResult().getType(), tracker_, rewriter);
2203 }
2204};
2205
2207class UpdateMemberDefTypeFromWrite final : public OpRewritePattern<MemberDefOp> {
2208 ConversionTracker &tracker_;
2209
2210public:
2211 UpdateMemberDefTypeFromWrite(MLIRContext *ctx, ConversionTracker &tracker)
2212 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
2213
2214 LogicalResult matchAndRewrite(MemberDefOp op, PatternRewriter &rewriter) const override {
2215 // Find all uses of the member symbol name within its parent struct.
2217 assert(parentRes && "MemberDefOp parent is always StructDefOp"); // per ODS def
2218
2219 // If the symbol is used by a MemberWriteOp with a different result type then change
2220 // the type of the MemberDefOp to match the MemberWriteOp result type.
2221 Type newType = nullptr;
2222 if (auto memberUsers = llzk::getSymbolUses(op, parentRes)) {
2223 std::optional<Location> newTypeLoc = std::nullopt;
2224 for (SymbolTable::SymbolUse symUse : memberUsers.value()) {
2225 if (MemberWriteOp writeOp = llvm::dyn_cast<MemberWriteOp>(symUse.getUser())) {
2226 Type writeToType = writeOp.getVal().getType();
2227 LLVM_DEBUG(llvm::dbgs() << "[UpdateMemberDefTypeFromWrite] checking " << writeOp << '\n');
2228 if (!newType) {
2229 // If a new type has not yet been discovered, store the new type.
2230 newType = writeToType;
2231 newTypeLoc = writeOp.getLoc();
2232 } else if (writeToType != newType) {
2233 // Typically, there will only be one write for each member of a struct but do not rely
2234 // on that assumption. If multiple writes with a different types A and B are found where
2235 // A->B is a legal conversion (i.e., more concrete unification), then it is safe to use
2236 // type B with the assumption that the write with type A will be updated by another
2237 // pattern to also use type B.
2238 if (!tracker_.isLegalConversion(writeToType, newType, "UpdateMemberDefTypeFromWrite")) {
2239 if (tracker_.isLegalConversion(
2240 newType, writeToType, "UpdateMemberDefTypeFromWrite"
2241 )) {
2242 // 'writeToType' is the more concrete type
2243 newType = writeToType;
2244 newTypeLoc = writeOp.getLoc();
2245 } else {
2246 // Give an error if the types are incompatible.
2247 return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) {
2248 diag.append(
2249 "Cannot update type of '", MemberDefOp::getOperationName(),
2250 "' because there are multiple '", MemberWriteOp::getOperationName(),
2251 "' with different value types"
2252 );
2253 if (newTypeLoc) {
2254 diag.attachNote(newTypeLoc).append("type written here is ", newType);
2255 }
2256 diag.attachNote(writeOp.getLoc()).append("type written here is ", writeToType);
2257 });
2258 }
2259 }
2260 }
2261 }
2262 }
2263 }
2264 if (!newType || newType == op.getType()) {
2265 return failure(); // nothing changed
2266 }
2267 if (!tracker_.isLegalConversion(op.getType(), newType, "UpdateMemberDefTypeFromWrite")) {
2268 return failure();
2269 }
2270 rewriter.modifyOpInPlace(op, [&op, &newType]() { op.setType(newType); });
2271 LLVM_DEBUG(llvm::dbgs() << "[UpdateMemberDefTypeFromWrite] updated type of " << op << '\n');
2272 return success();
2273 }
2274};
2275
2276namespace {
2277
2278SmallVector<std::unique_ptr<Region>> moveRegions(Operation *op) {
2279 SmallVector<std::unique_ptr<Region>> newRegions;
2280 for (Region &region : op->getRegions()) {
2281 auto newRegion = std::make_unique<Region>();
2282 newRegion->takeBody(region);
2283 newRegions.push_back(std::move(newRegion));
2284 }
2285 return newRegions;
2286}
2287
2288} // namespace
2289
2292class UpdateInferredResultTypes final : public OpTraitRewritePattern<OpTrait::InferTypeOpAdaptor> {
2293 ConversionTracker &tracker_;
2294
2295public:
2296 UpdateInferredResultTypes(MLIRContext *ctx, ConversionTracker &tracker)
2297 : OpTraitRewritePattern(ctx, 6), tracker_(tracker) {}
2298
2299 LogicalResult matchAndRewrite(Operation *op, PatternRewriter &rewriter) const override {
2300 SmallVector<Type, 1> inferredResultTypes;
2301 InferTypeOpInterface retTypeFn = llvm::cast<InferTypeOpInterface>(op);
2302 LogicalResult result = retTypeFn.inferReturnTypes(
2303 op->getContext(), op->getLoc(), op->getOperands(), op->getRawDictionaryAttrs(),
2304 op->getPropertiesStorage(), op->getRegions(), inferredResultTypes
2305 );
2306 if (failed(result)) {
2307 return failure();
2308 }
2309 if (op->getResultTypes() == inferredResultTypes) {
2310 return failure(); // nothing changed
2311 }
2312 if (!tracker_.areLegalConversions(
2313 op->getResultTypes(), inferredResultTypes, "UpdateInferredResultTypes"
2314 )) {
2315 return failure();
2316 }
2317
2318 // Move nested region bodies and replace the original op with the updated types list.
2319 LLVM_DEBUG(llvm::dbgs() << "[UpdateInferredResultTypes] replaced " << *op);
2320 SmallVector<std::unique_ptr<Region>> newRegions = moveRegions(op);
2321 Operation *newOp = rewriter.create(
2322 op->getLoc(), op->getName().getIdentifier(), op->getOperands(), inferredResultTypes,
2323 op->getAttrs(), op->getSuccessors(), newRegions
2324 );
2325 rewriter.replaceOp(op, newOp);
2326 LLVM_DEBUG(llvm::dbgs() << " with " << *newOp << '\n');
2327 return success();
2328 }
2329};
2330
2332class UpdateFuncTypeFromReturn final : public OpRewritePattern<FuncDefOp> {
2333 ConversionTracker &tracker_;
2334
2335public:
2336 UpdateFuncTypeFromReturn(MLIRContext *ctx, ConversionTracker &tracker)
2337 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
2338
2339 LogicalResult matchAndRewrite(FuncDefOp op, PatternRewriter &rewriter) const override {
2340 Region &body = op.getFunctionBody();
2341 if (body.empty()) {
2342 return failure();
2343 }
2344 ReturnOp retOp = llvm::dyn_cast<ReturnOp>(body.back().getTerminator());
2345 assert(retOp && "final op in body region must be return");
2346 OperandRange::type_range tyFromReturnOp = retOp.getOperands().getTypes();
2347
2348 FunctionType oldFuncTy = op.getFunctionType();
2349 if (oldFuncTy.getResults() == tyFromReturnOp) {
2350 return failure(); // nothing changed
2351 }
2352 if (!tracker_.areLegalConversions(
2353 oldFuncTy.getResults(), tyFromReturnOp, "UpdateFuncTypeFromReturn"
2354 )) {
2355 return failure();
2356 }
2357
2358 rewriter.modifyOpInPlace(op, [&]() {
2359 op.setFunctionType(rewriter.getFunctionType(oldFuncTy.getInputs(), tyFromReturnOp));
2360 });
2361 LLVM_DEBUG(
2362 llvm::dbgs() << "[UpdateFuncTypeFromReturn] changed " << op.getSymName() << " from "
2363 << oldFuncTy << " to " << op.getFunctionType() << '\n'
2364 );
2365 return success();
2366 }
2367};
2368
2373class UpdateFreeFuncCallOpTypes final : public OpRewritePattern<CallOp> {
2374 ConversionTracker &tracker_;
2375
2376public:
2377 UpdateFreeFuncCallOpTypes(MLIRContext *ctx, ConversionTracker &tracker)
2378 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
2379
2380 LogicalResult matchAndRewrite(CallOp op, PatternRewriter &rewriter) const override {
2381 if (calleeReferencesTemplateParam(op)) {
2382 return failure();
2383 }
2384 SymbolTableCollection tables;
2385 auto lookupRes = lookupTopLevelSymbol<FuncDefOp>(tables, op.getCalleeAttr(), op);
2386 if (failed(lookupRes)) {
2387 return failure();
2388 }
2389 FuncDefOp targetFunc = lookupRes->get();
2390 if (targetFunc.isInStruct()) {
2391 // this pattern only applies when the callee is NOT in a struct
2392 return failure();
2393 }
2394 if (op.getResultTypes() == targetFunc.getFunctionType().getResults()) {
2395 return failure(); // nothing changed
2396 }
2397 if (!tracker_.areLegalConversions(
2398 op.getResultTypes(), targetFunc.getFunctionType().getResults(),
2399 "UpdateFreeFuncCallOpTypes"
2400 )) {
2401 return failure();
2402 }
2403
2404 LLVM_DEBUG(llvm::dbgs() << "[UpdateFreeFuncCallOpTypes] replaced " << op);
2405 CallOp newOp = replaceOpWithNewOp<CallOp>(rewriter, op, targetFunc, op.getArgOperands());
2406 (void)newOp; // tell compiler it's intentionally unused in release builds
2407 LLVM_DEBUG(llvm::dbgs() << " with " << newOp << '\n');
2408 return success();
2409 }
2410};
2411
2412namespace {
2413
2414LogicalResult updateMemberRefValFromMemberDef(
2415 MemberRefOpInterface op, ConversionTracker &tracker, PatternRewriter &rewriter
2416) {
2417 SymbolTableCollection tables;
2418 auto def = op.getMemberDefOp(tables);
2419 if (failed(def)) {
2420 return failure();
2421 }
2422 Type oldResultType = op.getVal().getType();
2423 Type newResultType = def->get().getType();
2424 if (oldResultType == newResultType ||
2425 !tracker.isLegalConversion(oldResultType, newResultType, "updateMemberRefValFromMemberDef")) {
2426 return failure();
2427 }
2428 rewriter.modifyOpInPlace(op, [&op, &newResultType]() { op.getVal().setType(newResultType); });
2429 LLVM_DEBUG(
2430 llvm::dbgs() << "[updateMemberRefValFromMemberDef] updated value type in " << op << '\n'
2431 );
2432 return success();
2433}
2434
2435} // namespace
2436
2438class UpdateMemberReadValFromDef final : public OpRewritePattern<MemberReadOp> {
2439 ConversionTracker &tracker_;
2440
2441public:
2442 UpdateMemberReadValFromDef(MLIRContext *ctx, ConversionTracker &tracker)
2443 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
2444
2445 LogicalResult matchAndRewrite(MemberReadOp op, PatternRewriter &rewriter) const override {
2446 return updateMemberRefValFromMemberDef(op, tracker_, rewriter);
2447 }
2448};
2449
2451class UpdateMemberWriteValFromDef final : public OpRewritePattern<MemberWriteOp> {
2452 ConversionTracker &tracker_;
2453
2454public:
2455 UpdateMemberWriteValFromDef(MLIRContext *ctx, ConversionTracker &tracker)
2456 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
2457
2458 LogicalResult matchAndRewrite(MemberWriteOp op, PatternRewriter &rewriter) const override {
2459 return updateMemberRefValFromMemberDef(op, tracker_, rewriter);
2460 }
2461};
2462
2463LogicalResult run(ModuleOp modOp, ConversionTracker &tracker) {
2464 MLIRContext *ctx = modOp.getContext();
2465 RewritePatternSet patterns(ctx);
2466 patterns.add<
2467 // Benefit of this one must be higher than rules that would propagate the type in the opposite
2468 // direction (ex: `UpdateArrayElemFromArrRead`) else the greedy conversion would not converge.
2469 // benefit = 6
2470 UpdateInferredResultTypes, // OpTrait::InferTypeOpAdaptor (ReadArrayOp, ExtractArrayOp)
2471 // benefit = 3
2472 UpdateFreeFuncCallOpTypes, // CallOp, targeting non-struct functions
2473 UpdateFuncTypeFromReturn, // FuncDefOp
2474 UpdateNewArrayElemFromWrite, // CreateArrayOp
2475 UpdateArrayElemFromArrRead, // ReadArrayOp
2476 UpdateArrayElemFromArrWrite, // WriteArrayOp
2477 UpdateMemberDefTypeFromWrite, // MemberDefOp
2478 UpdateMemberReadValFromDef, // MemberReadOp
2479 UpdateMemberWriteValFromDef // MemberWriteOp
2480 >(ctx, tracker);
2481
2482 return applyAndFoldGreedily(modOp, tracker, std::move(patterns));
2483}
2484} // namespace Step5_PropagateTypes
2485
2486namespace Step6_Cleanup {
2487
2488struct FromKeepSet : public CleanupBase {
2490
2493 static bool hasTemplateSymbolBindings(Operation *op) {
2494 if (StructDefOp sdef = llvm::dyn_cast<StructDefOp>(op)) {
2495 return sdef.hasTemplateSymbolBindings();
2496 }
2497 if (llvm::isa<function::FuncDefOp>(op)) {
2498 if (TemplateOp parent = getParentOfType<TemplateOp>(op)) {
2499 return parent.hasConstOps<TemplateSymbolBindingOpInterface>();
2500 }
2501 }
2502 return false;
2503 }
2504
2508 LogicalResult eraseUnreachableFrom(ArrayRef<SymbolOpInterface> keep) {
2509 // Initialize roots from the given symbol definitions.
2510 SetVector<SymbolOpInterface> roots(keep.begin(), keep.end());
2511 // Add GlobalDefOp to the set of roots.
2512 rootMod.walk([&roots](global::GlobalDefOp gdef) { roots.insert(gdef); });
2513
2514 // Use a SymbolDefTree to find all Symbol defs reachable from one of the root nodes. Then
2515 // collect all Symbol uses reachable from those def nodes. These are the symbols that should
2516 // be preserved. All other symbol defs should be removed.
2517 DenseSet<Operation *> defsToKeep;
2518 llvm::df_iterator_default_set<const SymbolUseGraphNode *> symbolsToKeep;
2519 for (size_t i = 0; i < roots.size(); ++i) { // iterate for safe insertion
2520 SymbolOpInterface keepRoot = roots[i];
2521 LLVM_DEBUG({ llvm::dbgs() << "[EraseUnreachable] root: " << keepRoot << '\n'; });
2522 const SymbolDefTreeNode *keepRootNode = defTree.lookupNode(keepRoot);
2523 assert(keepRootNode && "every symbol def must be in the def tree");
2524 for (const SymbolDefTreeNode *reachableDefNode : llvm::depth_first(keepRootNode)) {
2525 LLVM_DEBUG({
2526 llvm::dbgs() << "[EraseUnreachable] can reach: " << reachableDefNode->getOp() << '\n';
2527 });
2528 if (SymbolOpInterface reachableDef = reachableDefNode->getOp()) {
2529 if (isErasableDefinition(reachableDef.getOperation())) {
2530 defsToKeep.insert(reachableDef.getOperation());
2531 }
2532 // Use 'depth_first_ext()' to get all symbol uses reachable from the current Symbol def
2533 // node. There are no uses if the node is not in the graph. Within the loop that populates
2534 // 'depth_first_ext()', also check if the symbol is an erasable definition and ensure it
2535 // is in 'roots' so the outer loop preserves all symbols reachable from it.
2536 if (const SymbolUseGraphNode *useGraphNodeForDef = useGraph.lookupNode(reachableDef)) {
2537 for (const SymbolUseGraphNode *usedSymbolNode :
2538 depth_first_ext(useGraphNodeForDef, symbolsToKeep)) {
2539 LLVM_DEBUG({
2540 llvm::dbgs() << "[EraseUnreachable] uses symbol: "
2541 << usedSymbolNode->getSymbolPath() << '\n';
2542 });
2543 // Ignore struct/template parameter symbols (before doing the lookup below because it
2544 // would fail anyway and then cause the "failed" case to be triggered unnecessarily).
2545 if (usedSymbolNode->isTemplateSymbolBinding()) {
2546 continue;
2547 }
2548 // If `usedSymbolNode` references an erasable definition, ensure it's considered in
2549 // the roots so symbols reachable from its body are preserved too.
2550 auto lookupRes = usedSymbolNode->lookupSymbol(tables);
2551 if (failed(lookupRes)) {
2552 LLVM_DEBUG(useGraph.dumpToDotFile());
2553 return failure();
2554 }
2555 // If loaded via an IncludeOp it's not in the current AST anyway so ignore.
2556 if (lookupRes->viaInclude()) {
2557 continue;
2558 }
2559 Operation *usedOp = lookupRes->get();
2560 if (isErasableDefinition(usedOp)) {
2561 SymbolOpInterface asSymbol = llvm::cast<SymbolOpInterface>(usedOp);
2562 bool insertRes = roots.insert(asSymbol);
2563 (void)insertRes; // tell compiler it's intentionally unused in release builds
2564 LLVM_DEBUG({
2565 if (insertRes) {
2566 llvm::dbgs() << "[EraseUnreachable] found another root: " << asSymbol << '\n';
2567 }
2568 });
2569 }
2570 }
2571 }
2572 }
2573 }
2574 }
2575
2576 SmallVector<SymbolOpInterface> toErase;
2577 rootMod.walk([this, &defsToKeep, &symbolsToKeep, &toErase](Operation *op) {
2578 if (!isErasableDefinition(op) || defsToKeep.contains(op)) {
2579 return;
2580 }
2581 SymbolOpInterface symOp = llvm::cast<SymbolOpInterface>(op);
2582 const SymbolUseGraphNode *n = this->useGraph.lookupNode(symOp);
2583 if (!n || !symbolsToKeep.contains(n)) {
2584 LLVM_DEBUG(llvm::dbgs() << "[EraseUnreachable] removing: " << symOp.getNameAttr() << '\n');
2585 toErase.push_back(symOp);
2586 }
2587 });
2588 for (SymbolOpInterface symOp : toErase) {
2589 symOp.erase();
2590 }
2591
2592 return success();
2593 }
2594};
2595
2596} // namespace Step6_Cleanup
2597
2598class PassImpl : public llzk::polymorphic::impl::FlatteningPassBase<PassImpl> {
2599 using Base = FlatteningPassBase<PassImpl>;
2600 using Base::Base;
2601
2603 FlatteningCleanupMode getEffectiveCleanupMode() const {
2604 FlatteningCleanupMode m = cleanupMode.getValue();
2606 }
2607
2608 void runOnOperation() override {
2609 ModuleOp modOp = getOperation();
2610 if (failed(runOn(modOp))) {
2611 LLVM_DEBUG({
2612 // If the pass failed, dump the current IR.
2613 llvm::dbgs() << "=====================================================================\n";
2614 llvm::dbgs() << " Dumping module after failure of pass " << DEBUG_TYPE << '\n';
2615 modOp.print(llvm::dbgs(), OpPrintingFlags().assumeVerified());
2616 llvm::dbgs() << "=====================================================================\n";
2617 });
2618 signalPassFailure();
2619 }
2620 }
2621
2622 inline LogicalResult runOn(ModuleOp modOp) {
2623 FlatteningCleanupMode effectiveCleanupMode = getEffectiveCleanupMode();
2624 // If the cleanup mode is set to remove anything not reachable from the main struct, do an
2625 // initial pass to remove things that are not reachable (as an optimization) because creating
2626 // an instantiated version of a struct will not cause something to become reachable that was
2627 // not already reachable in parameterized form.
2628 if (effectiveCleanupMode == FlatteningCleanupMode::MainAsRoot) {
2629 if (failed(eraseUnreachableFromMainStruct(modOp))) {
2630 return failure();
2631 }
2632 }
2633
2634 // Pass Manager to run some standard cleanup passes that are always beneficial:
2635 // - Remove templates that contain no struct or function definitions
2636 // - Convert templates with no constant parameters or expressions into modules
2637 OpPassManager universalCleanup(ModuleOp::getOperationName());
2638 universalCleanup.addPass(createEmptyTemplateRemovalPass());
2639
2640 // Run universal cleanup as a preliminary step to satisfy the
2641 // `assert(!isNullOrEmpty(paramNames))` precondition in `genClone()`.
2642 if (failed(runPipeline(universalCleanup, modOp))) {
2643 return failure();
2644 }
2645
2646 ConversionTracker tracker;
2647 if (failed(Step1_InstantiateStructs::instantiateMainStruct(modOp, tracker))) {
2648 llvm::errs() << DEBUG_TYPE << " failed while instantiating the main struct\n";
2649 return failure();
2650 }
2651
2652 unsigned loopCount = 0;
2653 do {
2654 ++loopCount;
2655 if (loopCount > iterationLimit) {
2656 llvm::errs() << DEBUG_TYPE << " exceeded the limit of " << iterationLimit
2657 << " iterations!\n";
2658 return failure();
2659 }
2660 tracker.resetModifiedFlag();
2661
2662 LLVM_DEBUG({
2663 llvm::dbgs() << "[FlatteningPass(count=" << loopCount
2664 << ")] Running step 1: struct instantiation\n";
2665 });
2666 // Find calls to "compute()" that return a parameterized struct type and replace it to call an
2667 // instantiated version of the struct that has parameters replaced with the constant values.
2668 // Create the necessary instantiated/flattened struct in the same location as the original.
2669 if (failed(Step1_InstantiateStructs::run(modOp, tracker))) {
2670 llvm::errs() << DEBUG_TYPE << " failed while instantiating structs in templates\n";
2671 return failure();
2672 }
2673 // Instantiate calls to templated functions.
2674 if (failed(Step2_InstantiateFunctions::run(modOp, tracker))) {
2675 llvm::errs() << DEBUG_TYPE << " failed while instantiating functions in templates\n";
2676 return failure();
2677 }
2678
2679 LLVM_DEBUG({
2680 llvm::dbgs() << "[FlatteningPass(count=" << loopCount
2681 << ")] Running step 2: loop unrolling\n";
2682 });
2683 // Unroll loops with known iterations.
2684 if (failed(Step3_Unroll::run(modOp, tracker))) {
2685 llvm::errs() << DEBUG_TYPE << " failed while unrolling loops\n";
2686 return failure();
2687 }
2688
2689 LLVM_DEBUG({
2690 llvm::dbgs() << "[FlatteningPass(count=" << loopCount
2691 << ")] Running step 3: affine maps instantiation\n";
2692 });
2693 // Instantiate affine_map parameters of StructType and ArrayType.
2694 if (failed(Step4_InstantiateAffineMaps::run(modOp, tracker))) {
2695 llvm::errs() << DEBUG_TYPE << " failed while instantiating `affine_map` parameters\n";
2696 return failure();
2697 }
2698
2699 LLVM_DEBUG({
2700 llvm::dbgs() << "[FlatteningPass(count=" << loopCount
2701 << ")] Running step 4: type propagation\n";
2702 });
2703 // Propagate updated types using the semantics of various ops.
2704 if (failed(Step5_PropagateTypes::run(modOp, tracker))) {
2705 llvm::errs() << DEBUG_TYPE << " failed while propagating instantiated types\n";
2706 return failure();
2707 }
2708
2709 LLVM_DEBUG(if (tracker.isModified()) {
2710 llvm::dbgs() << "=====================================================================\n";
2711 llvm::dbgs() << " Dumping module between iterations of " << DEBUG_TYPE << '\n';
2712 modOp.print(llvm::dbgs(), OpPrintingFlags().assumeVerified());
2713 llvm::dbgs() << "=====================================================================\n";
2714 });
2715 } while (tracker.isModified());
2716
2717 tracker.clearPartialFuncInstantiations();
2718
2719 // Run user-selected cleanup first.
2720 if (failed(cleanupSwitch(modOp, tracker))) {
2721 return failure();
2722 }
2723 // Run universal cleanup again since no-param or param-only structs may exist now.
2724 if (failed(runPipeline(universalCleanup, modOp))) {
2725 return failure();
2726 }
2727
2728 OpPassManager allocationCleanup(ModuleOp::getOperationName());
2729 allocationCleanup.addPass(createRemoveUnusedDiscardableAllocationsPass(
2730 RemoveUnusedDiscardableAllocationsPassOptions {
2731 .allocatorOpName = CreateArrayOp::getOperationName().str()
2732 }
2733 ));
2734 return runPipeline(allocationCleanup, modOp);
2735 }
2736
2737 // Perform cleanup according to the 'cleanupMode' option.
2738 LogicalResult cleanupSwitch(ModuleOp modOp, const ConversionTracker &tracker) {
2739 FlatteningCleanupMode effectiveCleanupMode = getEffectiveCleanupMode();
2740 LLVM_DEBUG({ llvm::dbgs() << "[FlatteningPass] Running step 5: cleanup "; });
2741 switch (effectiveCleanupMode) {
2742 case FlatteningCleanupMode::MainAsRoot:
2743 LLVM_DEBUG(llvm::dbgs() << "(main as root mode)\n");
2744 return eraseUnreachableFromMainStruct(modOp, false);
2745 case FlatteningCleanupMode::ConcreteAsRoot:
2746 LLVM_DEBUG(llvm::dbgs() << "(concrete definitions mode)\n");
2747 return eraseUnreachableFromConcreteDefinitions(modOp);
2748 case FlatteningCleanupMode::Preimage:
2749 LLVM_DEBUG(llvm::dbgs() << "(preimage mode)\n");
2750 return erasePreimageOfInstantiations(modOp, tracker);
2751 case FlatteningCleanupMode::Unspecified:
2752 default:
2753 LLVM_DEBUG(llvm::dbgs() << "(disabled)\n");
2754 return success();
2755 }
2756 }
2757
2758 // Erase parameterized definitions that were replaced with concrete instantiations.
2759 LogicalResult erasePreimageOfInstantiations(ModuleOp rootMod, const ConversionTracker &tracker) {
2760 // TODO: The names from getInstantiatedDefinitionNames() are NOT guaranteed to be paths from the
2761 // "top root" and they also do not indicate a root module so there could be ambiguity. This is a
2762 // broader problem in the FlatteningPass itself so let's just assume, for now, that these are
2763 // paths from the "top root". See [LLZK-286].
2764 FromEraseSet cleaner(
2765 rootMod, getAnalysis<SymbolDefTree>(), getAnalysis<SymbolUseGraph>(),
2766 tracker.getInstantiatedDefinitionNames()
2767 );
2768 LogicalResult res = cleaner.eraseUnusedDefinitions();
2769 if (succeeded(res)) {
2770 LLVM_DEBUG(llvm::dbgs() << "[Cleanup(preimage)] success\n";);
2771 // Warn about any definitions that were instantiated but still have uses elsewhere.
2772 const SymbolUseGraph *useGraph = nullptr;
2773 rootMod->walk([this, &cleaner, &useGraph](Operation *walkedOp) {
2774 SymbolOpInterface op = llvm::dyn_cast<SymbolOpInterface>(walkedOp);
2775 if (!op || !cleaner.getTryToEraseSet().contains(op)) {
2776 return;
2777 }
2778 // If needed, rebuild use graph to reflect deletions.
2779 if (!useGraph) {
2780 useGraph = &getAnalysis<SymbolUseGraph>();
2781 }
2782 // If the op has any users, report the warning.
2783 if (useGraph->lookupNode(op)->hasPredecessor()) {
2784 op.emitWarning("Parameterized definition still has uses!").report();
2785 }
2786 });
2787 } else {
2788 LLVM_DEBUG(llvm::dbgs() << "[Cleanup(preimage)] failed\n";);
2789 }
2790 return res;
2791 }
2792
2793 LogicalResult eraseUnreachableFromConcreteDefinitions(ModuleOp rootMod) {
2794 SmallVector<SymbolOpInterface> roots;
2795 rootMod.walk([&roots](Operation *op) {
2796 if (isErasableDefinition(op) && !Step6_Cleanup::FromKeepSet::hasTemplateSymbolBindings(op)) {
2797 roots.push_back(llvm::cast<SymbolOpInterface>(op));
2798 }
2799 });
2800
2801 Step6_Cleanup::FromKeepSet cleaner(
2802 rootMod, getAnalysis<SymbolDefTree>(), getAnalysis<SymbolUseGraph>()
2803 );
2804 return cleaner.eraseUnreachableFrom(roots);
2805 }
2806
2807 LogicalResult eraseUnreachableFromMainStruct(ModuleOp rootMod, bool emitWarning = true) {
2808 Step6_Cleanup::FromKeepSet cleaner(
2809 rootMod, getAnalysis<SymbolDefTree>(), getAnalysis<SymbolUseGraph>()
2810 );
2811 FailureOr<SymbolLookupResult<StructDefOp>> mainOpt =
2812 getMainInstanceDef(cleaner.tables, rootMod.getOperation());
2813 if (failed(mainOpt)) {
2814 return failure();
2815 }
2816 SymbolLookupResult<StructDefOp> main = mainOpt.value();
2817 if (emitWarning && !main) {
2818 // Emit warning if there is no main specified because all cleanup-candidate definitions not
2819 // reachable from global defs may be removed.
2820 rootMod.emitWarning()
2821 .append(
2822 "using option '", cleanupMode.getArgStr(), '=',
2823 stringifyFlatteningCleanupMode(FlatteningCleanupMode::MainAsRoot), "' with no \"",
2825 "\" attribute on the top-level module may remove all cleanup-candidate definitions!"
2826 )
2827 .report();
2828 }
2829 SmallVector<SymbolOpInterface> roots;
2830 if (main) {
2831 roots.push_back(*main);
2832 }
2833 return cleaner.eraseUnreachableFrom(roots);
2834 }
2835};
2836
2837} // namespace
#define DEBUG_TYPE
Common private implementation for poly dialect passes.
This file defines methods symbol lookup across LLZK operations and included files.
::mlir::TypedValue<::llzk::array::ArrayType > getArrRef()
Gets the SSA Value for the referenced array.
inline ::llzk::array::ArrayType getArrRefType()
Gets the type of the referenced array.
ArrayType cloneWith(std::optional<::llvm::ArrayRef< int64_t > > shape, ::mlir::Type elementType) const
Clone this type with the given shape and element type.
::mlir::Type getElementType() const
static ArrayType get(::mlir::Type elementType, ::llvm::ArrayRef<::mlir::Attribute > dimensionSizes)
Definition Types.cpp.inc:83
::llvm::ArrayRef<::mlir::Attribute > getDimensionSizes() const
::mlir::TypedValue<::llzk::array::ArrayType > getResult()
Definition Ops.h.inc:408
::mlir::DenseI32ArrayAttr getNumDimsPerMapAttr()
Definition Ops.h.inc:421
static constexpr ::llvm::StringLiteral getOperationName()
Definition Ops.h.inc:377
::mlir::OperandRangeRange getMapOperands()
Definition Ops.h.inc:392
::mlir::TypedValue<::mlir::Type > getResult()
Definition Ops.h.inc:923
::mlir::TypedValue<::llzk::array::ArrayType > getArrRef()
Definition Ops.h.inc:899
::mlir::TypedValue<::llzk::array::ArrayType > getArrRef()
Definition Ops.h.inc:1070
::mlir::TypedValue<::mlir::Type > getRvalue()
Definition Ops.h.inc:1078
static constexpr ::llvm::StringLiteral getOperationName()
Definition Ops.h.inc:353
void setType(::mlir::Type attrValue)
Definition Ops.cpp.inc:556
::std::optional<::mlir::Attribute > getTableOffset()
Definition Ops.cpp.inc:979
void setTableOffsetAttr(::mlir::Attribute attr)
Definition Ops.h.inc:753
::mlir::Value getVal()
Gets the SSA Value that holds the read/write data for the MemberRefOp.
::mlir::FailureOr< SymbolLookupResult< MemberDefOp > > getMemberDefOp(::mlir::SymbolTableCollection &tables)
Gets the definition for the member referenced in this op.
Definition Ops.cpp:687
static constexpr ::llvm::StringLiteral getOperationName()
Definition Ops.h.inc:942
StructType getType(::std::optional<::mlir::ArrayAttr > constParams={})
Gets the StructType representing this struct.
static constexpr ::llvm::StringLiteral getOperationName()
Definition Ops.h.inc:1170
::llvm::StringRef getSymName()
Definition Ops.cpp.inc:1608
void setSymName(::llvm::StringRef attrValue)
Definition Ops.cpp.inc:1613
::mlir::SymbolRefAttr getNameRef() const
static StructType get(::mlir::SymbolRefAttr structName)
Definition Types.cpp.inc:79
::mlir::FailureOr< SymbolLookupResult< StructDefOp > > getDefinition(::mlir::SymbolTableCollection &symbolTable, ::mlir::Operation *op, bool reportMissing=true) const
Gets the struct op that defines this struct.
Definition Types.cpp:26
::mlir::ArrayAttr getParams() const
bool calleeIsStructConstrain()
Return true iff the callee function name is FUNC_NAME_CONSTRAIN within a StructDefOp.
Definition Ops.cpp:1195
::llzk::component::StructType getSingleResultTypeOfCompute()
Assuming the callee is FUNC_NAME_COMPUTE, return the single StructType result.
Definition Ops.cpp:1218
::mlir::SymbolRefAttr getCalleeAttr()
Definition Ops.h.inc:292
::llvm::LogicalResult verifySymbolUses(::mlir::SymbolTableCollection &symbolTable)
Definition Ops.cpp:1111
bool calleeIsStructCompute()
Return true iff the callee function name is FUNC_NAME_COMPUTE within a StructDefOp.
Definition Ops.cpp:1183
::mlir::SymbolRefAttr getCallee()
Definition Ops.cpp.inc:470
::mlir::FunctionType getTypeSignature()
Return the FunctionType inferred from the arg operands and result types of this CallOp.
Definition Ops.cpp:1151
void setTemplateParamsAttr(::mlir::ArrayAttr attr)
Definition Ops.h.inc:316
::mlir::Operation::operand_range getArgOperands()
Definition Ops.h.inc:266
::mlir::ArrayAttr getTemplateParamsAttr()
Definition Ops.h.inc:297
::mlir::OperandRangeRange getMapOperands()
Definition Ops.h.inc:270
void setCalleeAttr(::mlir::SymbolRefAttr attr)
Definition Ops.h.inc:312
::mlir::FailureOr< UnificationMap > unifyTypeSignature(::mlir::FunctionType other)
Attempt type unfication between the inferred FunctionType from this CallOp (as LHS) and the given Fun...
Definition Ops.cpp:1155
::mlir::LogicalResult verifyTemplateParamsMatchInferred(::llvm::iterator_range<::mlir::Region::op_iterator<::llzk::polymorphic::TemplateParamOp > > targetParamDefs, const UnificationMap &unifications)
Verify that each template parameter value provided in this CallOp is consistent with the value inferr...
Definition Ops.cpp:713
::mlir::LogicalResult verifyTemplateParamCompatibility(::mlir::Attribute paramFromCallOp, ::llzk::polymorphic::TemplateParamOp targetParam)
Check type compatibility of the given template parameter value from this CallOp against the declared ...
::mlir::FailureOr<::llzk::SymbolLookupResult<::llzk::function::FuncDefOp > > getCalleeTarget(::mlir::SymbolTableCollection &tables)
Resolve and return the target FuncDefOp for this CallOp.
Definition Ops.cpp:1211
::mlir::DenseI32ArrayAttr getNumDimsPerMapAttr()
Definition Ops.h.inc:302
FuncDefOp clone(::mlir::IRMapping &mapper)
Create a deep copy of this function and all of its blocks, remapping any operands that use values out...
::mlir::FunctionType getFunctionType()
Definition Ops.cpp.inc:984
::llvm::ArrayRef<::mlir::Type > getArgumentTypes()
Required by FunctionOpInterface.
Definition Ops.h.inc:883
::llzk::component::StructType getSingleResultTypeOfCompute()
Assuming the name is FUNC_NAME_COMPUTE, return the single StructType result.
Definition Ops.cpp:505
::llvm::StringRef getSymName()
Definition Ops.cpp.inc:979
bool isStructCompute()
Return true iff the function is within a StructDefOp and named FUNC_NAME_COMPUTE.
Definition Ops.h.inc:912
bool isInStruct()
Return true iff the function is within a StructDefOp.
Definition Ops.h.inc:909
void setFunctionType(::mlir::FunctionType attrValue)
Definition Ops.cpp.inc:1003
void setSymName(::llvm::StringRef attrValue)
Definition Ops.cpp.inc:999
::mlir::StringAttr getSymNameAttr()
Definition Ops.h.inc:716
::mlir::Operation::operand_range getOperands()
Definition Ops.h.inc:1024
::mlir::FlatSymbolRefAttr getConstNameAttr()
Definition Ops.h.inc:465
::mlir::StringAttr getSymNameAttr()
Definition Ops.h.inc:674
::mlir::Region & getInitializerRegion()
Definition Ops.h.inc:661
::llvm::StringRef getSymName()
Definition Ops.cpp.inc:838
::mlir::Region & getBodyRegion()
Definition Ops.h.inc:873
bool hasConstNamed(::mlir::StringRef find)
Return true if there is an op of type OpT with the given name within the body region.
Definition Ops.h.inc:951
::mlir::StringAttr getSymNameAttr()
Definition Ops.h.inc:886
void setSymName(::llvm::StringRef attrValue)
Definition Ops.cpp.inc:1064
::llvm::StringRef getSymName()
Definition Ops.cpp.inc:1059
inline ::llvm::iterator_range<::mlir::Region::op_iterator< OpT > > getConstOps()
Return ops of type OpT within the body region.
Definition Ops.h.inc:923
::mlir::StringAttr getSymNameAttr()
Definition Ops.h.inc:1175
::mlir::FlatSymbolRefAttr getNameRef() const
Shared state for post-instantiation cleanup helpers.
Definition SharedImpl.h:67
CleanupBase(mlir::ModuleOp root, const SymbolDefTree &symDefTree, const SymbolUseGraph &symUseGraph)
int main(int argc, char **argv)
std::string toStringList(InputIt begin, InputIt end)
Generate a comma-separated string representation by traversing elements from begin to end where the e...
Definition Debug.h:156
component::StructType getStructTypeWithParams(mlir::SymbolRefAttr nameRef, mlir::ArrayAttr params)
Build a struct type while representing an empty parameter list as absent.
Definition SharedImpl.h:121
mlir::ConversionTarget newConverterDefinedTargetWithCallback(mlir::TypeConverter &tyConv, mlir::MLIRContext *ctx, LegalityCheckCallback &cb, AdditionalChecks &&...checks)
Return a new ConversionTarget allowing all LLZK-required dialects and defining Op legality based on t...
Definition SharedImpl.h:199
mlir::ConversionTarget newConverterDefinedTarget(mlir::TypeConverter &tyConv, mlir::MLIRContext *ctx, AdditionalChecks &&...checks)
Return a new ConversionTarget allowing all LLZK-required dialects and defining Op legality based on t...
Definition SharedImpl.h:183
FailureOr< InstantiationLayout > buildInstantiationLayout(TemplateOp parentTemplate, ArrayAttr callParams, const DenseMap< Attribute, Attribute > &paramNameToConcrete)
void setInstantiationNamePattern(TemplateOp templateOp, ArrayAttr namePattern)
bool isErasableDefinition(mlir::Operation *op)
Return true iff op is a cleanup candidate.
std::unique_ptr<::mlir::Pass > createEmptyTemplateRemovalPass()
::llvm::StringRef stringifyFlatteningCleanupMode(FlatteningCleanupMode val)
OpClass replaceOpWithNewOp(Rewriter &rewriter, mlir::Operation *op, Args &&...args)
Wrapper for PatternRewriter::replaceOpWithNewOp() that automatically copies discardable attributes (i...
std::unique_ptr<::mlir::Pass > createRemoveUnusedDiscardableAllocationsPass()
bool typeListsUnify(Iter1 lhs, Iter2 rhs, mlir::ArrayRef< llvm::StringRef > rhsReversePrefix={}, UnificationMap *unifications=nullptr)
Return true iff the two lists of Type instances are equivalent or could be equivalent after full inst...
Definition TypeHelper.h:271
bool isConcreteType(Type type, bool allowStructParams)
mlir::FailureOr< SymbolLookupResultUntyped > lookupTopLevelSymbol(mlir::SymbolTableCollection &tables, mlir::SymbolRefAttr symbol, mlir::Operation *origin, bool reportMissing=true)
FailureOr< StructType > getMainInstanceType(Operation *lookupFrom)
std::optional< mlir::SymbolTable::UseRange > getSymbolUses(mlir::Operation *from)
Get an iterator range for all of the uses, for any symbol, that are nested within the given operation...
TypeClass getIfSingleton(mlir::TypeRange types)
Definition TypeHelper.h:302
AttrConcreteness
Concreteness classification for an argument to a parameterized struct type.
Definition TypeHelper.h:117
mlir::DenseMap< std::pair< mlir::SymbolRefAttr, Side >, mlir::Attribute > UnificationMap
Optional result from type unifications.
Definition TypeHelper.h:217
std::string stringWithoutType(mlir::Attribute a)
bool isNullOrEmpty(mlir::ArrayAttr a)
SymbolRefAttr appendLeaf(SymbolRefAttr orig, FlatSymbolRefAttr newLeaf)
OpClass getParentOfType(mlir::Operation *op)
Return the closest surrounding parent/ancestor operation that is of type 'OpClass'.
Definition OpHelpers.h:53
AttrConcreteness classifyAttrConcreteness(Attribute attr, bool allowStructParams)
ArrayType flattenArrayElementType(ArrayType outerArrTy, Type elementType)
TypeClass getAtIndex(mlir::TypeRange types, size_t index)
Definition TypeHelper.h:306
mlir::RewritePatternSet newGeneralRewritePatternSet(mlir::TypeConverter &tyConv, mlir::MLIRContext *ctx, mlir::ConversionTarget &target)
Return a new RewritePatternSet covering all LLZK op types that may contain a StructType.
mlir::SymbolRefAttr asSymbolRefAttr(mlir::StringAttr root, mlir::SymbolRefAttr tail)
Build a SymbolRefAttr that prepends tail with root, i.e., root::tail.
int64_t fromAPInt(const llvm::APInt &i)
FailureOr< SymbolLookupResult< StructDefOp > > getMainInstanceDef(SymbolTableCollection &symbolTable, Operation *lookupFrom)
bool isMoreConcreteUnification(Type oldTy, Type newTy, llvm::function_ref< bool(Type oldTy, Type newTy)> knownOldToNew)
llvm::SmallVector< FlatSymbolRefAttr > getPieces(SymbolRefAttr ref)
constexpr char MAIN_ATTR_NAME[]
Name of the attribute on the top-level ModuleOp that specifies the type of the main struct.
Definition Constants.h:37
Groups the information needed after concrete parameters have been chosen to decide how to name a new ...
Definition SharedImpl.h:136
mlir::ArrayAttr namePattern
The refined literal chunks; fully concrete generated templates clear this state.
Definition SharedImpl.h:143
mlir::SmallVector< mlir::Attribute > remainingNames
Definition SharedImpl.h:137
mlir::ArrayAttr concreteParamKey
Ordered [parameter-name, concrete-value, ...] entries for exact partial-function reuse.
Definition SharedImpl.h:139