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"
72
73#define DEBUG_TYPE "llzk-flatten"
74
75using namespace mlir;
76using namespace llzk;
77using namespace llzk::array;
78using namespace llzk::component;
79using namespace llzk::constrain;
80using namespace llzk::felt;
81using namespace llzk::function;
82using namespace llzk::polymorphic;
83using namespace llzk::polymorphic::detail;
84
85namespace {
86
87class ConversionTracker {
92 struct PartialFuncInstantiation {
93 ArrayAttr concreteParamKey;
94 StringAttr templateName;
95 StringAttr functionName;
96 };
97
99 bool modified;
102 DenseMap<StructType, StructType> structInstantiations;
104 DenseMap<StructType, StructType> reverseInstantiations;
106 DenseSet<SymbolRefAttr> funcInstantiations;
109 DenseMap<Operation *, SmallVector<PartialFuncInstantiation>> partialFuncInstantiations;
112 DenseMap<StructType, SmallVector<Diagnostic>> delayedDiagnostics;
113
114public:
115 bool isModified() const { return modified; }
116 void resetModifiedFlag() { modified = false; }
117 void updateModifiedFlag(bool currStepModified) { modified |= currStepModified; }
118
119 void recordInstantiation(StructType oldType, StructType newType) {
120 assert(!isNullOrEmpty(oldType.getParams()) && "cannot instantiate with no params");
121
122 auto forwardResult = structInstantiations.try_emplace(oldType, newType);
123 if (forwardResult.second) {
124 // Insertion was successful
125 // ASSERT: The reverse map does not contain this mapping either
126 assert(!reverseInstantiations.contains(newType));
127 reverseInstantiations[newType] = oldType;
128 // Set the modified flag
129 modified = true;
130 } else {
131 // ASSERT: If a mapping already existed for `oldType` it must be `newType`
132 assert(forwardResult.first->getSecond() == newType);
133 // ASSERT: The reverse mapping is already present as well
134 assert(reverseInstantiations.lookup(newType) == oldType);
135 }
136 assert(structInstantiations.size() == reverseInstantiations.size());
137 }
138
140 std::optional<StructType> getInstantiation(StructType oldType) const {
141 auto cachedResult = structInstantiations.find(oldType);
142 if (cachedResult != structInstantiations.end()) {
143 return cachedResult->second;
144 }
145 return std::nullopt;
146 }
147
149 void recordInstantiation(SymbolRefAttr funcName) {
150 funcInstantiations.insert(funcName);
151 modified = true;
152 }
153
155 std::optional<SymbolRefAttr>
156 lookupPartialFuncInstantiation(FuncDefOp sourceFunc, ArrayAttr concreteParamKey) const {
157 auto found = partialFuncInstantiations.find(sourceFunc.getOperation());
158 if (found == partialFuncInstantiations.end()) {
159 return std::nullopt;
160 }
161 for (const PartialFuncInstantiation &candidate : found->second) {
162 if (candidate.concreteParamKey == concreteParamKey) {
163 SmallVector<FlatSymbolRefAttr> calleeSuffix {
164 FlatSymbolRefAttr::get(candidate.templateName),
165 FlatSymbolRefAttr::get(candidate.functionName),
166 };
167 return asSymbolRefAttr(calleeSuffix);
168 }
169 }
170 return std::nullopt;
171 }
172
174 void recordPartialFuncInstantiation(
175 FuncDefOp sourceFunc, ArrayAttr concreteParamKey, TemplateOp templateOp, FuncDefOp functionOp
176 ) {
177 assert(
178 !lookupPartialFuncInstantiation(sourceFunc, concreteParamKey).has_value() &&
179 "partial function instantiation already cached"
180 );
181 partialFuncInstantiations[sourceFunc.getOperation()].push_back(
182 PartialFuncInstantiation {
183 concreteParamKey,
184 templateOp.getSymNameAttr(),
185 functionOp.getSymNameAttr(),
186 }
187 );
188 }
189
192 void clearPartialFuncInstantiations() { partialFuncInstantiations.clear(); }
193
195 DenseSet<SymbolRefAttr> getInstantiatedDefinitionNames() const {
196 DenseSet<SymbolRefAttr> instantiatedNames = funcInstantiations;
197 for (const auto &[origRemoteTy, _] : structInstantiations) {
198 instantiatedNames.insert(origRemoteTy.getNameRef());
199 }
200 return instantiatedNames;
201 }
202
203 void reportDelayedDiagnostics(StructType newType, CallOp caller) {
204 auto res = delayedDiagnostics.find(newType);
205 if (res != delayedDiagnostics.end()) {
206 llzk::polymorphic::detail::reportDelayedDiagnostics(caller, std::move(res->second));
207
208 // Emitting a Diagnostic consumes it (per DiagnosticEngine::emit) so remove them from the map.
209 // Unfortunately, this means if the key StructType is the result of instantiation at multiple
210 // `compute()` calls it will only be reported at one of those locations, not all.
211 delayedDiagnostics.erase(newType);
212 }
214
215 SmallVector<Diagnostic> &delayedDiagnosticSet(StructType newType) {
216 return delayedDiagnostics[newType];
221 bool isLegalConversion(Type oldType, Type newType, const char *patName) const {
222 std::function<bool(Type, Type)> checkInstantiations = [&](Type oTy, Type nTy) {
223 // Check if `oTy` is a struct with a known instantiation to `nTy`
224 if (StructType oldStructType = llvm::dyn_cast<StructType>(oTy)) {
225 // Note: The values in `structInstantiations` must be no-parameter struct types
226 // so there is no need for recursive check, simple equality is sufficient.
227 if (this->structInstantiations.lookup(oldStructType) == nTy) {
228 return true;
229 }
231 // Check if `nTy` is the result of a struct instantiation and if the pre-image of
232 // that instantiation (i.e., the parameterized version of the instantiated struct)
233 // is a more concrete unification of `oTy`.
234 if (StructType newStructType = llvm::dyn_cast<StructType>(nTy)) {
235 if (auto preImage = this->reverseInstantiations.lookup(newStructType)) {
236 if (isMoreConcreteUnification(oTy, preImage, checkInstantiations)) {
237 return true;
238 }
240 }
241 return false;
242 };
243
244 if (isMoreConcreteUnification(oldType, newType, checkInstantiations)) {
245 return true;
246 }
247 LLVM_DEBUG(
248 llvm::dbgs() << "[" << patName << "] Cannot replace old type " << oldType
249 << " with new type " << newType
250 << " because it does not define a compatible and more concrete type.\n";
251 );
252 return false;
253 }
254
255 template <typename T, typename U>
256 inline bool areLegalConversions(T oldTypes, U newTypes, const char *patName) const {
257 return llvm::all_of(
258 llvm::zip_equal(oldTypes, newTypes), [this, &patName](std::tuple<Type, Type> oldThenNew) {
259 return this->isLegalConversion(std::get<0>(oldThenNew), std::get<1>(oldThenNew), patName);
260 }
261 );
262 }
263};
264
267struct MatchFailureListener : public RewriterBase::Listener {
268 bool hadFailure = false;
269
270 ~MatchFailureListener() override {}
271
272 void notifyMatchFailure(Location loc, function_ref<void(Diagnostic &)> reasonCallback) override {
273 hadFailure = true;
274
275 InFlightDiagnostic diag = emitError(loc);
276 reasonCallback(*diag.getUnderlyingDiagnostic());
277 diag.report();
278 }
279};
280
281static LogicalResult
282applyAndFoldGreedily(ModuleOp modOp, ConversionTracker &tracker, RewritePatternSet &&patterns) {
283 bool currStepModified = false;
284 MatchFailureListener failureListener;
285 LogicalResult result = applyPatternsGreedily(
286 modOp->getRegion(0), std::move(patterns),
287 GreedyRewriteConfig().enableFolding().setMaxIterations(20).setListener(&failureListener),
288 &currStepModified
289 );
290 tracker.updateModifiedFlag(currStepModified);
291 return failure(result.failed() || failureListener.hadFailure);
292}
293
295template <bool AllowStructParams = true> bool isConcreteAttr(Attribute a) {
296 return classifyAttrConcreteness(a, AllowStructParams) == AttrConcreteness::Concrete;
297}
298
299static bool calleeReferencesTemplateParam(CallOp op) {
300 SymbolRefAttr callee = op.getCalleeAttr();
301 if (!callee || callee.getNestedReferences().size() != 1) {
302 return false;
303 }
304 TemplateOp parentTemplate = getParentOfType<TemplateOp>(op);
305 if (!parentTemplate) {
306 return false;
307 }
308 return parentTemplate.hasConstNamed<TemplateParamOp>(callee.getRootReference());
312
315class StructCloner {
316 ConversionTracker &tracker_;
317 ModuleOp rootMod;
318 SymbolTableCollection symTables;
319 bool reportMissing = true;
320
321 FailureOr<StructType> genClone(StructType typeAtCaller, ArrayRef<Attribute> typeAtCallerParams) {
322 LLVM_DEBUG(llvm::dbgs() << "[StructCloner] attempting clone of " << typeAtCaller << '\n');
323 // Find the StructDefOp for the original StructType
324 FailureOr<SymbolLookupResult<StructDefOp>> r =
325 typeAtCaller.getDefinition(symTables, rootMod, reportMissing);
326 if (failed(r)) {
327 LLVM_DEBUG(llvm::dbgs() << "[StructCloner] skip: cannot find StructDefOp \n");
328 return failure(); // getDefinition() already emits a sufficient error message
329 }
330 LLVM_DEBUG(llvm::dbgs() << "[StructCloner] found definition\n";);
331
332 StructDefOp origStruct = r->get();
333 StructType typeAtDef = origStruct.getType();
334 MLIRContext *ctx = origStruct.getContext();
335 TemplateOp parentTemplate = getParentOfType<TemplateOp>(origStruct);
336 assert(parentTemplate && "parameterized struct must be nested in a TemplateOp");
337 ModuleOp parentModule = getParentOfType<ModuleOp>(parentTemplate);
338 assert(parentModule && "TemplateOp must be nested in a ModuleOp");
339
340 // Map of StructDefOp parameter name to concrete Attribute at the current instantiation site.
341 DenseMap<Attribute, Attribute> paramNameToConcrete;
342 // Reduced from `typeAtCallerParams` to contain only the non-concrete Attributes.
343 ArrayAttr reducedCallerParams = nullptr;
344 SmallVector<Attribute> nonConcreteParams;
345 {
346 ArrayAttr paramNames = typeAtDef.getParams();
347
348 // pre-conditions
349 assert(!isNullOrEmpty(paramNames));
350 assert(paramNames.size() == typeAtCallerParams.size());
351
352 for (size_t i = 0, e = paramNames.size(); i < e; ++i) {
353 Attribute next = typeAtCallerParams[i];
354 if (isConcreteAttr<false>(next)) {
355 paramNameToConcrete[paramNames[i]] = next;
356 } else {
357 nonConcreteParams.push_back(next);
358 }
359 }
360 // post-conditions
361 assert(nonConcreteParams.size() + paramNameToConcrete.size() == paramNames.size());
362
363 if (paramNameToConcrete.empty()) {
364 LLVM_DEBUG(llvm::dbgs() << "[StructCloner] skip: no concrete params \n");
365 return failure();
366 }
367 if (!nonConcreteParams.empty()) {
368 reducedCallerParams = ArrayAttr::get(ctx, nonConcreteParams);
369 }
370 }
371
372 FailureOr<InstantiationLayout> layoutResult =
373 buildInstantiationLayout(parentTemplate, ArrayAttr(), paramNameToConcrete);
374 if (failed(layoutResult)) {
375 return failure();
376 }
377 InstantiationLayout layout = std::move(*layoutResult);
378 assert(layout.remainingNames.size() == nonConcreteParams.size());
379
380 // This list will be used to build the new remote/external type.
381 SmallVector<FlatSymbolRefAttr> typeAtCallerSymPieces = getPieces(typeAtCaller.getNameRef());
382 typeAtCallerSymPieces.pop_back(); // drop struct name
383
384 // Evaluate any poly.expr symbols whose param dependencies are now concrete; add them to the
385 // map so ClonedBodyConstReadOpPattern can replace uses of those symbols too.
386 evaluateTemplateExprs(parentTemplate, paramNameToConcrete);
387
388 // Clone the original struct.
389 StructDefOp newStruct = origStruct.clone();
390 convertCalleesInPlace(newStruct, paramNameToConcrete);
391 if (layout.remainingNames.empty()) { // FULL INSTANTIATION CASE
392 // Set name of the new struct by prepending its name with instantiated template name.
393 newStruct.setSymName(
394 (layout.templateNameWithAttrs + mlir::Twine('_') + newStruct.getSymName()).str()
395 );
396 // Insert 'newStruct' into the parent ModuleOp of the original TemplateOp. Use the
397 // `SymbolTable::insert()` function so that the name will be made unique if necessary.
398 symTables.getSymbolTable(parentModule).insert(newStruct, Block::iterator(parentTemplate));
399 // Drop the old template name from the list.
400 typeAtCallerSymPieces.pop_back();
401 } else { // PARTIAL INSTANTIATION CASE
402 // Clone the template and set instantiated name.
403 TemplateOp newTemplate = parentTemplate.cloneWithoutRegions();
404 newTemplate.setSymName(layout.templateNameWithAttrs);
405 setInstantiationNamePattern(newTemplate, layout.namePattern);
406 assert(newTemplate->getNumRegions() > 0 && "region exists"); // it just doesn't have a block
407 newTemplate.getBodyRegion().emplaceBlock();
408
409 // Clone preserved const param/expr ops.
410 for (Attribute name : layout.remainingNames) {
411 FlatSymbolRefAttr nameSym = llvm::dyn_cast<FlatSymbolRefAttr>(name);
412 assert(nameSym && "expected FlatSymbolRefAttr");
413
414 Operation *symOp = symTables.getSymbolTable(parentTemplate).lookup(nameSym.getAttr());
415 assert(symOp && "symbol must exist");
416 newTemplate.insert(newTemplate.begin(), symOp->clone());
417 }
418
419 // Insert the struct into the template and the template into the module. Use the
420 // `SymbolTable::insert()` function so that the name will be made unique if necessary.
421 symTables.getSymbolTable(newTemplate).insert(newStruct);
422 symTables.getSymbolTable(parentModule).insert(newTemplate, Block::iterator(parentTemplate));
423
424 // Replace the old template name in the list with the new one (get template name after
425 // symbol table insertion since it may be modified to make it unique).
426 typeAtCallerSymPieces.back() = FlatSymbolRefAttr::get(newTemplate.getSymNameAttr());
427 }
428
429 // Retrieve the new type AFTER inserting since the struct name may be appended to make
430 // it unique and use the remaining non-concrete parameters from the original type.
431 StructType newLocalType = newStruct.getType(reducedCallerParams);
432 typeAtCallerSymPieces.push_back(
433 FlatSymbolRefAttr::get(newLocalType.getNameRef().getLeafReference())
434 );
435 StructType newRemoteType =
436 StructType::get(asSymbolRefAttr(typeAtCallerSymPieces), newLocalType.getParams());
437 LLVM_DEBUG({
438 llvm::dbgs() << "[StructCloner] original def type: " << typeAtDef << '\n';
439 llvm::dbgs() << "[StructCloner] cloned def type: " << newStruct.getType() << '\n';
440 llvm::dbgs() << "[StructCloner] original remote type: " << typeAtCaller << '\n';
441 llvm::dbgs() << "[StructCloner] cloned local type: " << newLocalType << '\n';
442 llvm::dbgs() << "[StructCloner] cloned remote type: " << newRemoteType << '\n';
443 });
444
445 // Within the new struct, replace all references to the original StructType (i.e., the
446 // locally-parameterized version) with the new locally-parameterized StructType,
447 // and replace all uses of the removed struct parameters with the concrete values.
448 if (failed(substituteStructBody(
449 newStruct, typeAtDef, paramNameToConcrete, tracker_.delayedDiagnosticSet(newLocalType)
450 ))) {
451 LLVM_DEBUG(llvm::dbgs() << "[StructCloner] instantiating body of struct failed \n");
452 return failure();
453 }
454 return newRemoteType;
455 }
456
457public:
458 StructCloner(ConversionTracker &tracker, ModuleOp root)
459 : tracker_(tracker), rootMod(root), symTables() {}
460
461 FailureOr<StructType> createInstantiatedClone(StructType orig) {
462 LLVM_DEBUG(llvm::dbgs() << "[StructCloner] orig: " << orig << '\n');
463 if (ArrayAttr params = orig.getParams()) {
464 return genClone(orig, params.getValue());
465 }
466 LLVM_DEBUG(llvm::dbgs() << "[StructCloner] skip: nullptr for params \n");
467 return failure();
468 }
469
470 void enableReportMissing() { reportMissing = true; }
471
472 void disableReportMissing() { reportMissing = false; }
473};
474
475class DisableReportMissing;
476
477class ParameterizedStructUseTypeConverter : public TypeConverter {
478 ConversionTracker &tracker_;
479 StructCloner cloner;
480
481 friend DisableReportMissing;
482
483public:
484 ParameterizedStructUseTypeConverter(ConversionTracker &tracker, ModuleOp root)
485 : TypeConverter(), tracker_(tracker), cloner(tracker, root) {
486
487 addConversion([](Type inputTy) { return inputTy; });
488
489 addConversion([this](StructType inputTy) -> StructType {
490 LLVM_DEBUG(
491 llvm::dbgs() << "[ParameterizedStructUseTypeConverter] attempting conversion of "
492 << inputTy << '\n';
493 );
494 // First check for a cached entry
495 if (auto opt = tracker_.getInstantiation(inputTy)) {
496 return opt.value();
497 }
498
499 // Otherwise, try to create a clone of the struct with instantiated params. If that can't be
500 // done, return the original type to indicate that it's still legal (for this step at least).
501 FailureOr<StructType> cloneRes = cloner.createInstantiatedClone(inputTy);
502 if (failed(cloneRes)) {
503 return inputTy;
504 }
505 StructType newTy = cloneRes.value();
506 LLVM_DEBUG(
507 llvm::dbgs() << "[ParameterizedStructUseTypeConverter] instantiating " << inputTy
508 << " as " << newTy << '\n'
509 );
510 tracker_.recordInstantiation(inputTy, newTy);
511 return newTy;
512 });
513
514 addConversion([this](ArrayType inputTy) {
515 return inputTy.cloneWith(convertType(inputTy.getElementType()));
516 });
517 }
518};
519
520class CallStructFuncPattern : public OpConversionPattern<CallOp> {
521 ConversionTracker &tracker_;
522
523public:
524 CallStructFuncPattern(TypeConverter &converter, MLIRContext *ctx, ConversionTracker &tracker)
525 // benefit>0 so this applies instead of CallOpClassReplacePattern
526 : OpConversionPattern<CallOp>(converter, ctx, /*benefit=*/1), tracker_(tracker) {}
527
528 LogicalResult matchAndRewrite(
529 CallOp op, OpAdaptor adapter, ConversionPatternRewriter &rewriter
530 ) const override {
531 LLVM_DEBUG(llvm::dbgs() << "[CallStructFuncPattern] CallOp: " << op << '\n');
532
533 // Convert the result types of the CallOp
534 SmallVector<Type> newResultTypes;
535 if (failed(getTypeConverter()->convertTypes(op.getResultTypes(), newResultTypes))) {
536 return op->emitError("Could not convert Op result types.");
537 }
538 LLVM_DEBUG({
539 llvm::dbgs() << "[CallStructFuncPattern] newResultTypes: "
540 << debug::toStringList(newResultTypes) << '\n';
541 });
542
543 // Update the callee to reflect the new struct target if necessary. These checks are based on
544 // `CallOp::calleeIsStructC*()` but the types must not come from the CallOp in this case.
545 // Instead they must come from the converted versions.
546 SymbolRefAttr calleeAttr = op.getCalleeAttr();
547 if (op.calleeIsStructCompute()) {
548 if (StructType newStTy = getIfSingleton<StructType>(newResultTypes)) {
549 LLVM_DEBUG(llvm::dbgs() << "[CallStructFuncPattern] newStTy: " << newStTy << '\n');
550 calleeAttr = appendLeaf(newStTy.getNameRef(), calleeAttr.getLeafReference());
551 tracker_.reportDelayedDiagnostics(newStTy, op);
552 }
553 } else if (op.calleeIsStructConstrain()) {
554 if (StructType newStTy = getAtIndex<StructType>(adapter.getArgOperands().getTypes(), 0)) {
555 LLVM_DEBUG(llvm::dbgs() << "[CallStructFuncPattern] newStTy: " << newStTy << '\n');
556 calleeAttr = appendLeaf(newStTy.getNameRef(), calleeAttr.getLeafReference());
557 }
558 }
559
560 LLVM_DEBUG(llvm::dbgs() << "[CallStructFuncPattern] replaced " << op);
562 rewriter, op, newResultTypes, calleeAttr, adapter.getMapOperands(),
563 op.getNumDimsPerMapAttr(), adapter.getArgOperands()
564 );
565 (void)newOp; // tell compiler it's intentionally unused in release builds
566 LLVM_DEBUG(llvm::dbgs() << " with " << newOp << '\n');
567 return success();
568 }
569};
570
571// This one ensures MemberDefOp types are converted even if there are no reads/writes to them.
572class MemberDefOpPattern : public OpConversionPattern<MemberDefOp> {
573public:
574 MemberDefOpPattern(TypeConverter &converter, MLIRContext *ctx, ConversionTracker &)
575 // benefit>0 so this applies instead of GeneralTypeReplacePattern<MemberDefOp>
576 : OpConversionPattern<MemberDefOp>(converter, ctx, /*benefit=*/1) {}
577
578 LogicalResult matchAndRewrite(
579 MemberDefOp op, OpAdaptor /*adapter*/, ConversionPatternRewriter &rewriter
580 ) const override {
581 LLVM_DEBUG(llvm::dbgs() << "[MemberDefOpPattern] MemberDefOp: " << op << '\n');
582
583 Type oldMemberType = op.getType();
584 Type newMemberType = getTypeConverter()->convertType(oldMemberType);
585 if (oldMemberType == newMemberType) {
586 return failure(); // nothing changed
587 }
588 rewriter.modifyOpInPlace(op, [&op, &newMemberType]() { op.setType(newMemberType); });
589 return success();
590 }
591};
592
595class DisableReportMissing : public LegalityCheckCallback {
596 ParameterizedStructUseTypeConverter &tyConv;
597
598public:
599 explicit DisableReportMissing(ParameterizedStructUseTypeConverter &tc) : tyConv(tc) {}
600
601 void checkStarted() override { tyConv.cloner.disableReportMissing(); }
602
603 void checkEnded(bool) override { tyConv.cloner.enableReportMissing(); }
604};
605
606LogicalResult run(ModuleOp modOp, ConversionTracker &tracker) {
607 MLIRContext *ctx = modOp.getContext();
608 ParameterizedStructUseTypeConverter tyConv(tracker, modOp);
609 DisableReportMissing drm(tyConv);
610 ConversionTarget target = newConverterDefinedTargetWithCallback<>(tyConv, ctx, drm);
611 RewritePatternSet patterns = newGeneralRewritePatternSet(tyConv, ctx, target);
612 patterns.add<CallStructFuncPattern, MemberDefOpPattern>(tyConv, ctx, tracker);
613 return applyPartialConversion(modOp, target, std::move(patterns));
614}
615
618LogicalResult instantiateMainStruct(ModuleOp modOp, ConversionTracker &tracker) {
619 FailureOr<StructType> mainTypeOpt = getMainInstanceType(modOp);
620 if (failed(mainTypeOpt)) {
621 return failure();
622 }
623
624 StructType mainType = mainTypeOpt.value();
625 if (!mainType || isNullOrEmpty(mainType.getParams()) || tracker.getInstantiation(mainType)) {
626 return success();
627 }
628
629 StructCloner cloner(tracker, modOp);
630 FailureOr<StructType> cloneRes = cloner.createInstantiatedClone(mainType);
631 if (failed(cloneRes)) {
632 return failure();
633 }
634
635 StructType instantiatedMainType = cloneRes.value();
636 tracker.recordInstantiation(mainType, instantiatedMainType);
637 modOp->setAttr(MAIN_ATTR_NAME, TypeAttr::get(instantiatedMainType));
638 return success();
639}
640
641} // namespace Step1_InstantiateStructs
642
644
647inline static LogicalResult failIncompatibleInferredParam(
648 CallOp op, PatternRewriter &rewriter, FlatSymbolRefAttr paramName, TemplateParamOp paramOp
649) {
650 LLVM_DEBUG(
651 llvm::dbgs() << "[InstantiateFuncAtCallOp] unification for param '" << paramName
652 << "': incompatible with specified param type. MUST FAIL!\n"
653 );
654 return rewriter.notifyMatchFailure(op, [&paramName, &paramOp](Diagnostic &diag) {
655 diag.append("inferred value for parameter '")
656 .append(paramName)
657 .append("' is incompatible with specified param type")
658 .attachNote(paramOp.getLoc())
659 .append("template parameter declared here");
660 });
661}
662
665class WildcardTypeBodyInferer final {
666 SymbolTableCollection &symTables_;
667 const DenseMap<Attribute, Attribute> &paramNameToConcrete_;
668 SmallVector<std::pair<Operation *, FlatSymbolRefAttr>> activeInferences_;
669
670public:
671 WildcardTypeBodyInferer(
672 SymbolTableCollection &symTables, const DenseMap<Attribute, Attribute> &paramNameToConcrete
673 )
674 : symTables_(symTables), paramNameToConcrete_(paramNameToConcrete) {}
675
676 std::optional<Attribute> infer(FuncDefOp func, FlatSymbolRefAttr paramName) {
677 if (llvm::any_of(activeInferences_, [&](const auto &e) {
678 return e.first == func.getOperation() && e.second == paramName;
679 })) {
680 return std::nullopt;
681 }
682 activeInferences_.emplace_back(func.getOperation(), paramName);
683
684 TemplateTypeConverter tyConv((paramNameToConcrete_));
685 std::optional<Attribute> inferred;
686 bool ambiguous = false;
687
688 // Record a concrete candidate unless it conflicts with an earlier one, in which
689 // case the wildcard is treated as ambiguous and left unresolved.
690 auto noteCandidate = [&inferred, &ambiguous](Attribute candidate) {
691 if (!candidate || !isConcreteAttr(candidate)) {
692 return WalkResult::advance();
693 }
694 if (!inferred.has_value()) {
695 inferred = candidate;
696 return WalkResult::advance();
697 }
698 if (*inferred != candidate) {
699 ambiguous = true;
700 return WalkResult::interrupt();
701 }
702 return WalkResult::advance();
703 };
704
705 WalkResult walkResult = func.walk([&](Operation *bodyOp) {
706 if (auto castOp = llvm::dyn_cast<UnifiableCastOp>(bodyOp)) {
707 Type inputTy = tyConv.convertType(castOp.getInput().getType());
708 Type resultTy = tyConv.convertType(castOp.getResult().getType());
709 if (auto inputTvar = llvm::dyn_cast<TypeVarType>(inputTy);
710 inputTvar && inputTvar.getNameRef() == paramName && isConcreteType(resultTy)) {
711 return noteCandidate(TypeAttr::get(resultTy));
712 }
713 if (auto resultTvar = llvm::dyn_cast<TypeVarType>(resultTy);
714 resultTvar && resultTvar.getNameRef() == paramName && isConcreteType(inputTy)) {
715 return noteCandidate(TypeAttr::get(inputTy));
716 }
717 return WalkResult::advance();
718 }
719
720 auto nestedCall = llvm::dyn_cast<CallOp>(bodyOp);
721 if (!nestedCall) {
722 return WalkResult::advance();
723 }
724
725 FailureOr<SymbolLookupResult<FuncDefOp>> nestedTgtOpt =
726 nestedCall.getCalleeTarget(symTables_);
727 if (failed(nestedTgtOpt)) {
728 return WalkResult::advance();
729 }
730 FuncDefOp nestedTgt = nestedTgtOpt->get();
731 auto nestedTemplate = llvm::dyn_cast<TemplateOp>(nestedTgt->getParentOp());
732 if (!nestedTemplate) {
733 return WalkResult::advance();
734 }
735
736 TypeRange nestedResultTypes = nestedTgt.getFunctionType().getResults();
737 for (auto [result, nestedResultTy] :
738 llvm::zip_equal(nestedCall.getResults(), nestedResultTypes)) {
739 Type convertedResultTy = tyConv.convertType(result.getType());
740 auto resultTvar = llvm::dyn_cast<TypeVarType>(convertedResultTy);
741 auto nestedTvar = llvm::dyn_cast<TypeVarType>(nestedResultTy);
742 if (!resultTvar || !nestedTvar || resultTvar.getNameRef() != paramName) {
743 continue;
744 }
745 if (std::optional<Attribute> candidate = inferFromExplicitNestedCallParams(
746 nestedCall, nestedTemplate, nestedTvar.getNameRef(), tyConv
747 )) {
748 WalkResult candidateResult = noteCandidate(*candidate);
749 if (candidateResult.wasInterrupted()) {
750 return candidateResult;
751 }
752 continue;
753 }
754 if (std::optional<Attribute> candidate = infer(nestedTgt, nestedTvar.getNameRef())) {
755 WalkResult candidateResult = noteCandidate(*candidate);
756 if (candidateResult.wasInterrupted()) {
757 return candidateResult;
758 }
759 }
760 }
761 return WalkResult::advance();
762 });
763
764 activeInferences_.pop_back();
765 if (ambiguous || (walkResult.wasInterrupted() && !inferred.has_value())) {
766 return std::nullopt;
767 }
768 return inferred;
769 }
770
771private:
772 std::optional<Attribute> inferFromExplicitNestedCallParams(
773 CallOp nestedCall, TemplateOp nestedTemplate, FlatSymbolRefAttr nestedParamName,
774 const TemplateTypeConverter &tyConv
775 ) const {
776 ArrayAttr nestedCallParams = nestedCall.getTemplateParamsAttr();
777 if (isNullOrEmpty(nestedCallParams)) {
778 return std::nullopt;
779 }
780
781 for (auto [paramOp, attr] :
782 llvm::zip_equal(nestedTemplate.getConstOps<TemplateParamOp>(), nestedCallParams)) {
783 auto paramName = FlatSymbolRefAttr::get(paramOp.getSymNameAttr());
784 if (paramName != nestedParamName) {
785 continue;
786 }
787 Attribute convertedAttr = tyConv.convertAttr(attr);
788 return isConcreteAttr(convertedAttr) ? std::make_optional(convertedAttr) : std::nullopt;
789 }
790 return std::nullopt;
791 }
792};
793
797static LogicalResult applyBodyConversions(
798 CallOp op, FuncDefOp newFunc, const DenseMap<Attribute, Attribute> &paramNameToConcrete
799) {
800 SmallVector<Diagnostic> delayedDiagnostics;
801 if (failed(substituteFunctionBody(newFunc, paramNameToConcrete, delayedDiagnostics))) {
802 return failure();
803 }
804 LLVM_DEBUG(llvm::dbgs() << "[InstantiateFuncAtCallOp] instantiated clone: " << newFunc << '\n');
805 llzk::polymorphic::detail::reportDelayedDiagnostics(op, std::move(delayedDiagnostics));
806
807 SymbolTableCollection tables;
808 WalkResult res = newFunc.walk([&tables](CallOp nestedCall) {
809 return WalkResult(nestedCall.verifySymbolUses(tables));
810 });
811 return failure(res.wasInterrupted());
812}
813
814class InstantiateFuncAtCallOp final : public OpRewritePattern<CallOp> {
815 ConversionTracker &tracker_;
816
817public:
818 InstantiateFuncAtCallOp(MLIRContext *ctx, ConversionTracker &tracker)
819 : OpRewritePattern<CallOp>(ctx), tracker_(tracker) {}
820
821 LogicalResult matchAndRewrite(CallOp op, PatternRewriter &rewriter) const override {
822 LLVM_DEBUG(llvm::dbgs() << "[InstantiateFuncAtCallOp] op: " << op << '\n');
823
824 if (calleeReferencesTemplateParam(op)) {
825 return failure();
826 }
827
828 // Lookup callee target function
829 SymbolTableCollection symTables;
830 FailureOr<SymbolLookupResult<FuncDefOp>> callTgtOpt = op.getCalleeTarget(symTables);
831 if (failed(callTgtOpt)) {
832 return rewriter.notifyMatchFailure(op, [](Diagnostic &diag) {
833 diag << "could not find target function for call";
834 });
835 }
836 FuncDefOp callTgt = callTgtOpt->get();
837
838 // Check if callee is within a TemplateOp
839 TemplateOp parentTemplate = llvm::dyn_cast<TemplateOp>(callTgt->getParentOp());
840 if (!parentTemplate) {
841 return failure(); // nothing to do if not parameterized
842 }
843 LLVM_DEBUG(
844 llvm::dbgs() << "[InstantiateFuncAtCallOp] target function in template "
845 << parentTemplate.getSymName() << '\n'
846 );
847
848 // Perform type unification with tracking to infer the instantiated type(s). Even though
849 // `CallOp` verification already checked that caller and callee types unify, the progress of
850 // instantiation so far may have brought together a chain of calls across templates where each
851 // individual unification check passed due to permissive type variables and/or symbols in the
852 // middle but the overall chain does not unify. Hence, this unification may fail and should
853 // produce a meaningful error message if it does.
854 // See: `test/Transforms/Flattening/instantiate_funcs_fail.llzk`
855 FailureOr<UnificationMap> unifyResult = unifyTypeSignature(op, callTgt, rewriter);
856 if (failed(unifyResult)) {
857 return failure();
858 }
859 LLVM_DEBUG(
860 llvm::dbgs() << "[InstantiateFuncAtCallOp] unifications of types: "
861 << debug::toStringList(unifyResult.value()) << '\n'
862 );
863
864 // Maps template parameter symbols to the instantiation value at the call site.
865 DenseMap<Attribute, Attribute> paramNameToConcrete;
866 if (failed(collectConcreteTemplateParams(
867 op, rewriter, symTables, callTgt, parentTemplate, unifyResult.value(),
868 paramNameToConcrete
869 ))) {
870 return failure();
871 }
872
873 if (paramNameToConcrete.empty()) {
874 LLVM_DEBUG(llvm::dbgs() << "[InstantiateFuncAtCallOp] skip: no concrete params\n");
875 return failure();
876 }
877
878 evaluateTemplateExprs(parentTemplate, paramNameToConcrete);
879
880 FailureOr<InstantiationLayout> layoutResult =
881 buildInstantiationLayout(parentTemplate, op.getTemplateParamsAttr(), paramNameToConcrete);
882 if (failed(layoutResult)) {
883 return failure();
884 }
885 InstantiationLayout layout = std::move(*layoutResult);
886 ModuleOp parentModule = getParentOfType<ModuleOp>(parentTemplate);
887 assert(parentModule && "TemplateOp must be nested in a ModuleOp");
888
889 SymbolRefAttr originalCalleeAttr = op.getCalleeAttr();
890 FailureOr<SymbolRefAttr> newCalleeAttr =
891 layout.remainingNames.empty()
892 ? instantiateFully(
893 op, rewriter, symTables, callTgt, parentTemplate, parentModule,
894 layout.templateNameWithAttrs, paramNameToConcrete
895 )
896 : instantiatePartially(
897 op, rewriter, symTables, callTgt, parentTemplate, parentModule, layout,
898 paramNameToConcrete, tracker_
899 );
900 if (failed(newCalleeAttr)) {
901 return failure();
902 }
903
904 tracker_.recordInstantiation(originalCalleeAttr);
905
906 // Update the CallOp to point to the instantiated function and mark the module as modified.
907 rewriter.modifyOpInPlace(op, [&op, &newCalleeAttr, &layout]() {
908 LLVM_DEBUG({
909 llvm::dbgs() << "[InstantiateFuncAtCallOp] updating callee from " << op.getCalleeAttr()
910 << " to " << *newCalleeAttr << '\n';
911 });
912 op.setCalleeAttr(*newCalleeAttr);
914 });
915 tracker_.updateModifiedFlag(true);
916 return success();
917 }
918
919private:
922 static FailureOr<UnificationMap>
923 unifyTypeSignature(CallOp op, FuncDefOp callTgt, PatternRewriter &rewriter) {
924 FailureOr<UnificationMap> unifyResult = op.unifyTypeSignature(callTgt.getFunctionType());
925 if (succeeded(unifyResult)) {
926 return unifyResult;
927 }
928 return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) {
929 diag.append("target function type does not unify with call type ")
930 .append(op.getTypeSignature())
931 .attachNote(callTgt.getLoc())
932 .append("target function declared here");
933 });
934 }
935
938 static LogicalResult collectConcreteTemplateParams(
939 CallOp op, PatternRewriter &rewriter, SymbolTableCollection &symTables, FuncDefOp callTgt,
940 TemplateOp parentTemplate, const UnificationMap &unifyResult,
941 DenseMap<Attribute, Attribute> &paramNameToConcrete
942 ) {
943 auto realParams = parentTemplate.getConstOps<TemplateParamOp>();
944 ArrayAttr callParams = op.getTemplateParamsAttr();
945 LLVM_DEBUG(
946 llvm::dbgs() << "[InstantiateFuncAtCallOp] TemplateParamsAttr: " << callParams << '\n'
947 );
948
949 auto recordConcreteParam = [&](FlatSymbolRefAttr paramName, TemplateParamOp paramOp,
950 Attribute concreteValue) {
951 if (failed(op.verifyTemplateParamValueCompatibility(concreteValue, paramOp))) {
952 return failIncompatibleInferredParam(op, rewriter, paramName, paramOp);
953 }
954 paramNameToConcrete[paramName] = concreteValue;
955 return success();
956 };
957
958 // If there's no template instantiation list, must infer all template parameters.
959 if (isNullOrEmpty(callParams)) {
960 for (auto paramOp : realParams) {
961 auto paramName = FlatSymbolRefAttr::get(paramOp.getSymNameAttr());
962 auto inferredValOpt = inferUnifiedParam(unifyResult, paramName);
963 if (!inferredValOpt.has_value()) {
964 LLVM_DEBUG(
965 llvm::dbgs() << "[InstantiateFuncAtCallOp] unification for param '" << paramName
966 << "': not found\n"
967 );
968 continue;
969 }
970 Attribute inferredVal = *inferredValOpt;
971 LLVM_DEBUG(
972 llvm::dbgs() << "[InstantiateFuncAtCallOp] inferredVal: " << inferredVal << '\n'
973 );
974 if (!isConcreteAttr(inferredVal)) {
975 LLVM_DEBUG(
976 llvm::dbgs() << "[InstantiateFuncAtCallOp] unification for param '" << paramName
977 << "': not concrete, " << inferredVal << '\n'
978 );
979 continue;
980 }
981 if (failed(recordConcreteParam(paramName, paramOp, inferredVal))) {
982 return failure();
983 }
984 }
985 return success();
986 }
987
988 // As stated earlier, need to run the verification checks again to ensure the
989 // instantiation is valid, except for the size check because that cannot change.
990 assert((callParams.size() == llvm::range_size(realParams)) && "per CallOpVerifier");
991 if (failed(op.verifyTemplateParamValuesCompatibility(realParams))) {
992 return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) {
993 diag.append("incompatible with specified param type(s)");
994 });
995 }
996 if (failed(op.verifyTemplateParamsMatchInferred(realParams, unifyResult))) {
997 return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) {
998 diag.append("incompatible with inferred param value(s)");
999 });
1000 }
1001
1002 // When template parameters are specified on the CallOp, use them as the source of truth
1003 // for concrete arguments, then infer wildcard parameters against the full explicit map.
1004 SmallVector<std::pair<TemplateParamOp, FlatSymbolRefAttr>> wildcardParams;
1005 for (auto [paramOp, attr] : llvm::zip_equal(realParams, callParams.getValue())) {
1006 auto paramName = FlatSymbolRefAttr::get(paramOp.getSymNameAttr());
1007 AttrConcreteness classification = classifyAttrConcreteness(attr);
1008 if (classification == AttrConcreteness::Concrete) {
1009 paramNameToConcrete[paramName] = attr;
1010 continue;
1011 }
1012
1013 if (classification == AttrConcreteness::NonConcrete) {
1014 LLVM_DEBUG(
1015 llvm::dbgs() << "[InstantiateFuncAtCallOp] unification for param '" << paramName
1016 << "': not concrete, " << attr << '\n'
1017 );
1018 continue;
1019 }
1020 wildcardParams.emplace_back(paramOp, paramName);
1021 }
1022
1023 WildcardTypeBodyInferer bodyInferer(symTables, paramNameToConcrete);
1024 for (auto [paramOp, paramName] : wildcardParams) {
1025 auto inferredValOpt = inferUnifiedParam(unifyResult, paramName);
1026 if (inferredValOpt.has_value() && isConcreteAttr(*inferredValOpt)) {
1027 LLVM_DEBUG(
1028 llvm::dbgs() << "[InstantiateFuncAtCallOp] inferredVal: " << *inferredValOpt << '\n'
1029 );
1030 if (failed(recordConcreteParam(paramName, paramOp, *inferredValOpt))) {
1031 return failure();
1032 }
1033 continue;
1034 }
1035
1036 inferredValOpt = bodyInferer.infer(callTgt, paramName);
1037 if (inferredValOpt.has_value() && isConcreteAttr(*inferredValOpt)) {
1038 LLVM_DEBUG(
1039 llvm::dbgs() << "[InstantiateFuncAtCallOp] body-inferred value for param '"
1040 << paramName << "': " << *inferredValOpt << '\n'
1041 );
1042 if (failed(recordConcreteParam(paramName, paramOp, *inferredValOpt))) {
1043 return failure();
1044 }
1045 }
1046 }
1047 return success();
1048 }
1049
1052 static FailureOr<SymbolRefAttr> instantiateFully(
1053 CallOp op, PatternRewriter &rewriter, SymbolTableCollection &symTables, FuncDefOp callTgt,
1054 TemplateOp parentTemplate, ModuleOp parentModule, StringRef templateNameWithAttrs,
1055 const DenseMap<Attribute, Attribute> &paramNameToConcrete
1056 ) {
1057 MLIRContext *ctx = op.getContext();
1058 std::string newFuncName =
1059 (mlir::Twine(templateNameWithAttrs) + "_" + callTgt.getSymName()).str();
1060 StringRef actualNewFuncName = newFuncName;
1061 if (!symTables.getSymbolTable(parentModule).lookup(newFuncName)) {
1062 FuncDefOp newFunc = callTgt.clone();
1063 newFunc.setSymName(newFuncName);
1064 convertCalleesInPlace(newFunc, paramNameToConcrete);
1065 // Insert before the TemplateOp; symbol table may adjust the name to ensure uniqueness.
1066 symTables.getSymbolTable(parentModule).insert(newFunc, Block::iterator(parentTemplate));
1067 actualNewFuncName = newFunc.getSymName();
1068 LLVM_DEBUG(
1069 llvm::dbgs() << "[InstantiateFuncAtCallOp] created full instantiation function: "
1070 << actualNewFuncName << '\n'
1071 );
1072 if (failed(applyBodyConversions(op, newFunc, paramNameToConcrete))) {
1073 LLVM_DEBUG(
1074 llvm::dbgs() << "[InstantiateFuncAtCallOp] body conversion failed for "
1075 << actualNewFuncName << '\n'
1076 );
1077 newFunc->erase();
1078 return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) {
1079 diag.append("failure while creating instantiated function '", actualNewFuncName, '\'');
1080 });
1081 }
1082 } else {
1083 LLVM_DEBUG(
1084 llvm::dbgs() << "[InstantiateFuncAtCallOp] reusing full instantiation function: "
1085 << actualNewFuncName << '\n'
1086 );
1087 }
1088
1089 // Callee: drop template & original function names, add the new module-level function name.
1090 // Original: @[prefix...]::@TemplateName::@funcName
1091 // New: @[prefix...]::@newFuncName
1092 SmallVector<FlatSymbolRefAttr> symPieces = getPieces(op.getCalleeAttr());
1093 assert(symPieces.size() >= 2 && "callee must include at least template and function names");
1094 symPieces.pop_back(); // remove original function name
1095 symPieces.pop_back(); // remove template name
1096 symPieces.push_back(FlatSymbolRefAttr::get(StringAttr::get(ctx, actualNewFuncName)));
1097 return asSymbolRefAttr(symPieces);
1098 }
1099
1104 static FailureOr<SymbolRefAttr> instantiatePartially(
1105 CallOp op, PatternRewriter &rewriter, SymbolTableCollection &symTables, FuncDefOp callTgt,
1106 TemplateOp parentTemplate, ModuleOp parentModule, const InstantiationLayout &layout,
1107 const DenseMap<Attribute, Attribute> &paramNameToConcrete, ConversionTracker &tracker
1108 ) {
1109 if (auto cached = tracker.lookupPartialFuncInstantiation(callTgt, layout.concreteParamKey)) {
1110 SmallVector<FlatSymbolRefAttr> symPieces = getPieces(op.getCalleeAttr());
1111 SmallVector<FlatSymbolRefAttr> cachedSuffix = getPieces(*cached);
1112 assert(symPieces.size() >= 2 && "callee must include at least template and function names");
1113 assert(cachedSuffix.size() == 2 && "cached callee suffix must contain template and function");
1114 symPieces.pop_back();
1115 symPieces.pop_back();
1116 symPieces.push_back(cachedSuffix[0]);
1117 symPieces.push_back(cachedSuffix[1]);
1118 SymbolRefAttr cachedCallee = asSymbolRefAttr(symPieces);
1119 LLVM_DEBUG(
1120 llvm::dbgs() << "[InstantiateFuncAtCallOp] reusing partial instantiation: "
1121 << cachedCallee << '\n'
1122 );
1123 return cachedCallee;
1124 }
1125 TemplateOp newTemplate = parentTemplate.cloneWithoutRegions();
1126 newTemplate.setSymName(layout.templateNameWithAttrs);
1127 setInstantiationNamePattern(newTemplate, layout.namePattern);
1128 assert(newTemplate->getNumRegions() > 0 && "region exists");
1129 newTemplate.getBodyRegion().emplaceBlock();
1130
1131 Block &newTemplateBody = newTemplate.getBodyRegion().front();
1132 for (Attribute name : layout.remainingNames) {
1133 FlatSymbolRefAttr nameSym = llvm::cast<FlatSymbolRefAttr>(name);
1134 Operation *paramOp = symTables.getSymbolTable(parentTemplate).lookup(nameSym.getAttr());
1135 assert(paramOp && "symbol must exist");
1136 newTemplateBody.push_back(paramOp->clone());
1137 }
1138
1139 // Clone and partially convert the function (concretize only the concrete params).
1140 FuncDefOp newFunc = callTgt.clone();
1141 convertCalleesInPlace(newFunc, paramNameToConcrete);
1142
1143 // Insert before body conversion so nested concrete callees verify from the root module. Use
1144 // SymbolTable::insert() so both physical symbol names are unique if necessary.
1145 symTables.getSymbolTable(newTemplate).insert(newFunc);
1146 symTables.getSymbolTable(parentModule).insert(newTemplate, Block::iterator(parentTemplate));
1147 if (failed(applyBodyConversions(op, newFunc, paramNameToConcrete))) {
1148 std::string newFuncName = newFunc.getSymName().str();
1149 LLVM_DEBUG(
1150 llvm::dbgs() << "[InstantiateFuncAtCallOp] body conversion failed for " << newFuncName
1151 << '\n'
1152 );
1153 newTemplate->erase();
1154 return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) {
1155 diag.append("failure while creating instantiated function '", newFuncName, '\'');
1156 });
1157 }
1158
1159 // Use the post-insertion names. The preferred template name may have collided.
1160 SmallVector<FlatSymbolRefAttr> symPieces = getPieces(op.getCalleeAttr());
1161 assert(symPieces.size() >= 2 && "callee must include at least template and function names");
1162 symPieces.pop_back();
1163 symPieces.pop_back(); // remove original template name
1164 symPieces.push_back(FlatSymbolRefAttr::get(newTemplate.getSymNameAttr()));
1165 symPieces.push_back(FlatSymbolRefAttr::get(newFunc.getSymNameAttr()));
1166 SymbolRefAttr newCallee = asSymbolRefAttr(symPieces);
1167
1168 LLVM_DEBUG(
1169 llvm::dbgs() << "[InstantiateFuncAtCallOp] created partial instantiation: " << newCallee
1170 << '\n'
1171 );
1172 // Publish only after insertion and body conversion have succeeded.
1173 tracker.recordPartialFuncInstantiation(callTgt, layout.concreteParamKey, newTemplate, newFunc);
1174 return newCallee;
1175 }
1176};
1177
1178LogicalResult run(ModuleOp modOp, ConversionTracker &tracker) {
1179 MLIRContext *ctx = modOp.getContext();
1180 RewritePatternSet patterns(ctx);
1181 patterns.add<InstantiateFuncAtCallOp>(ctx, tracker);
1182 MatchFailureListener failureListener;
1183 walkAndApplyPatterns(modOp, std::move(patterns), &failureListener);
1184 return failure(failureListener.hadFailure);
1185}
1186
1187} // namespace Step2_InstantiateFunctions
1188
1189namespace Step3_Unroll {
1190
1191// TODO: not guaranteed to work with WhileOp, can try with our custom attributes though.
1192template <HasInterface<LoopLikeOpInterface> OpClass>
1193class LoopUnrollPattern : public OpRewritePattern<OpClass> {
1194public:
1195 using OpRewritePattern<OpClass>::OpRewritePattern;
1196
1197 LogicalResult matchAndRewrite(OpClass loopOp, PatternRewriter &rewriter) const override {
1198 if (auto maybeConstant = getConstantTripCount(loopOp)) {
1199 uint64_t tripCount = *maybeConstant;
1200 if (tripCount == 0) {
1201 rewriter.eraseOp(loopOp);
1202 return success();
1203 } else if (tripCount == 1) {
1204 return loopOp.promoteIfSingleIteration(rewriter);
1205 }
1206 return loopUnrollByFactor(loopOp, tripCount);
1207 }
1208 return failure();
1209 }
1210
1211private:
1214 static std::optional<int64_t> getConstantTripCount(LoopLikeOpInterface loopOp) {
1215 std::optional<OpFoldResult> lbVal = loopOp.getSingleLowerBound();
1216 std::optional<OpFoldResult> ubVal = loopOp.getSingleUpperBound();
1217 std::optional<OpFoldResult> stepVal = loopOp.getSingleStep();
1218 if (!lbVal.has_value() || !ubVal.has_value() || !stepVal.has_value()) {
1219 return std::nullopt;
1220 }
1221 bool isSigned = true;
1222 if (auto forOp = dyn_cast<scf::ForOp>(loopOp.getOperation())) {
1223 isSigned = !forOp.getUnsignedCmp();
1224 }
1225 std::optional<APInt> result = constantTripCount(
1226 lbVal.value(), ubVal.value(), stepVal.value(), isSigned, scf::computeUbMinusLb
1227 );
1228 return result.has_value() ? std::optional<int64_t>(result->getZExtValue()) : std::nullopt;
1229 }
1230};
1231
1232LogicalResult run(ModuleOp modOp, ConversionTracker &tracker) {
1233 MLIRContext *ctx = modOp.getContext();
1234 RewritePatternSet patterns(ctx);
1235 patterns.add<LoopUnrollPattern<scf::ForOp>>(ctx);
1236 patterns.add<LoopUnrollPattern<affine::AffineForOp>>(ctx);
1237
1238 return applyAndFoldGreedily(modOp, tracker, std::move(patterns));
1239}
1240} // namespace Step3_Unroll
1241
1243
1244// Adapted from `mlir::getConstantIntValues()` but that one failed in CI for an unknown reason. This
1245// version uses a basic loop instead of llvm::map_to_vector().
1246std::optional<SmallVector<int64_t>> getConstantIntValues(ArrayRef<OpFoldResult> ofrs) {
1247 SmallVector<int64_t> res;
1248 for (OpFoldResult ofr : ofrs) {
1249 std::optional<int64_t> cv = getConstantIntValue(ofr);
1250 if (!cv.has_value()) {
1251 return std::nullopt;
1252 }
1253 res.push_back(cv.value());
1254 }
1255 return res;
1256}
1257
1258struct AffineMapFolder {
1259 struct Input {
1260 OperandRangeRange mapOpGroups;
1261 DenseI32ArrayAttr dimsPerGroup;
1262 ArrayRef<Attribute> paramsOfStructTy;
1263 };
1264
1265 struct Output {
1266 SmallVector<SmallVector<Value>> mapOpGroups;
1267 SmallVector<int32_t> dimsPerGroup;
1268 SmallVector<Attribute> paramsOfStructTy;
1269 };
1270
1271 static inline SmallVector<ValueRange> getConvertedMapOpGroups(Output out) {
1272 return llvm::map_to_vector(out.mapOpGroups, [](const SmallVector<Value> &grp) {
1273 return ValueRange(grp);
1274 });
1275 }
1276
1277 static LogicalResult
1278 fold(PatternRewriter &rewriter, const Input &in, Output &out, Operation *op, const char *aspect) {
1279 if (in.mapOpGroups.empty()) {
1280 // No affine map operands so nothing to do
1281 return failure();
1282 }
1283
1284 assert(in.mapOpGroups.size() <= in.paramsOfStructTy.size());
1285 assert(std::cmp_equal(in.mapOpGroups.size(), in.dimsPerGroup.size()));
1286
1287 size_t idx = 0; // index in `mapOpGroups`, i.e., the number of AffineMapAttr encountered
1288 for (Attribute sizeAttr : in.paramsOfStructTy) {
1289 if (AffineMapAttr m = dyn_cast<AffineMapAttr>(sizeAttr)) {
1290 ValueRange currMapOps = in.mapOpGroups[idx++];
1291 LLVM_DEBUG(
1292 llvm::dbgs() << "[AffineMapFolder] currMapOps: " << debug::toStringList(currMapOps)
1293 << '\n'
1294 );
1295 SmallVector<OpFoldResult> currMapOpsCast = getAsOpFoldResult(currMapOps);
1296 LLVM_DEBUG(
1297 llvm::dbgs() << "[AffineMapFolder] currMapOps as fold results: "
1298 << debug::toStringList(currMapOpsCast) << '\n'
1299 );
1300 if (auto constOps = Step4_InstantiateAffineMaps::getConstantIntValues(currMapOpsCast)) {
1301 SmallVector<Attribute> result;
1302 bool hasPoison = false; // indicates divide by 0 or mod by <1
1303 auto constAttrs = llvm::map_to_vector(*constOps, [&rewriter](int64_t v) -> Attribute {
1304 return rewriter.getIndexAttr(v);
1305 });
1306 LogicalResult foldResult = m.getAffineMap().constantFold(constAttrs, result, &hasPoison);
1307 if (hasPoison) {
1308 // Diagnostic remark: could be removed for release builds if too noisy
1309 op->emitRemark()
1310 .append(
1311 "Cannot fold affine_map for ", aspect, ' ', out.paramsOfStructTy.size(),
1312 " due to divide by 0 or modulus with negative divisor"
1313 )
1314 .report();
1315 return failure();
1316 }
1317 if (failed(foldResult)) {
1318 // Diagnostic remark: could be removed for release builds if too noisy
1319 op->emitRemark()
1320 .append(
1321 "Folding affine_map for ", aspect, ' ', out.paramsOfStructTy.size(), " failed"
1322 )
1323 .report();
1324 return failure();
1325 }
1326 if (result.size() != 1) {
1327 // Diagnostic remark: could be removed for release builds if too noisy
1328 op->emitRemark()
1329 .append(
1330 "Folding affine_map for ", aspect, ' ', out.paramsOfStructTy.size(),
1331 " produced ", result.size(), " results but expected 1"
1332 )
1333 .report();
1334 return failure();
1335 }
1336 assert(!llvm::isa<AffineMapAttr>(result[0]) && "not converted");
1337 out.paramsOfStructTy.push_back(result[0]);
1338 continue;
1339 }
1340 // If affine but not foldable, preserve the map ops
1341 out.mapOpGroups.emplace_back(currMapOps);
1342 out.dimsPerGroup.push_back(in.dimsPerGroup[idx - 1]); // idx was already incremented
1343 }
1344 // If not affine and foldable, preserve the original
1345 out.paramsOfStructTy.push_back(sizeAttr);
1346 }
1347 assert(idx == in.mapOpGroups.size() && "all affine_map not processed");
1348 assert(
1349 in.paramsOfStructTy.size() == out.paramsOfStructTy.size() &&
1350 "produced wrong number of dimensions"
1351 );
1352
1353 return success();
1354 }
1355};
1356
1358class InstantiateAtCreateArrayOp final : public OpRewritePattern<CreateArrayOp> {
1359 [[maybe_unused]]
1360 ConversionTracker &tracker_;
1361
1362public:
1363 InstantiateAtCreateArrayOp(MLIRContext *ctx, ConversionTracker &tracker)
1364 : OpRewritePattern(ctx), tracker_(tracker) {}
1365
1366 LogicalResult matchAndRewrite(CreateArrayOp op, PatternRewriter &rewriter) const override {
1367 ArrayType oldResultType = op.getType();
1368
1369 AffineMapFolder::Output out;
1370 AffineMapFolder::Input in = {
1371 op.getMapOperands(),
1373 oldResultType.getDimensionSizes(),
1374 };
1375 if (failed(AffineMapFolder::fold(rewriter, in, out, op, "array dimension"))) {
1376 return failure();
1377 }
1378
1379 ArrayType newResultType = ArrayType::get(oldResultType.getElementType(), out.paramsOfStructTy);
1380 if (newResultType == oldResultType) {
1381 return failure(); // nothing changed
1382 }
1383 // ASSERT: folding only preserves the original Attribute or converts affine to integer
1384 assert(tracker_.isLegalConversion(oldResultType, newResultType, "InstantiateAtCreateArrayOp"));
1385 LLVM_DEBUG(
1386 llvm::dbgs() << "[InstantiateAtCreateArrayOp] instantiating " << oldResultType << " as "
1387 << newResultType << " in \"" << op << "\"\n"
1388 );
1390 rewriter, op, newResultType, AffineMapFolder::getConvertedMapOpGroups(out), out.dimsPerGroup
1391 );
1392 return success();
1393 }
1394};
1395
1397class InstantiateAtCallOpCompute final : public OpRewritePattern<CallOp> {
1398 ConversionTracker &tracker_;
1399
1400public:
1401 InstantiateAtCallOpCompute(MLIRContext *ctx, ConversionTracker &tracker)
1402 : OpRewritePattern(ctx), tracker_(tracker) {}
1403
1404 LogicalResult matchAndRewrite(CallOp op, PatternRewriter &rewriter) const override {
1405 if (!op.calleeIsStructCompute()) {
1406 // this pattern only applies when the callee is "compute()" within a struct
1407 return failure();
1408 }
1409 LLVM_DEBUG(llvm::dbgs() << "[InstantiateAtCallOpCompute] target: " << op.getCallee() << '\n');
1411 LLVM_DEBUG(llvm::dbgs() << "[InstantiateAtCallOpCompute] oldRetTy: " << oldRetTy << '\n');
1412 ArrayAttr params = oldRetTy.getParams();
1413 if (isNullOrEmpty(params)) {
1414 // nothing to do if the StructType is not parameterized
1415 return failure();
1416 }
1417
1418 AffineMapFolder::Output out;
1419 AffineMapFolder::Input in = {
1420 op.getMapOperands(),
1422 params.getValue(),
1423 };
1424 if (!in.mapOpGroups.empty()) {
1425 // If there are affine map operands, attempt to fold them to a constant.
1426 if (failed(AffineMapFolder::fold(rewriter, in, out, op, "struct parameter"))) {
1427 return failure();
1428 }
1429 LLVM_DEBUG({
1430 llvm::dbgs() << "[InstantiateAtCallOpCompute] folded affine_map in result type params\n";
1431 });
1432 } else {
1433 // If there are no affine map operands, attempt to refine the result type of the CallOp using
1434 // the function argument types and the type of the target function.
1435 auto callArgTypes = op.getArgOperands().getTypes();
1436 if (callArgTypes.empty()) {
1437 // no refinement possible if no function arguments
1438 return failure();
1439 }
1440 if (calleeReferencesTemplateParam(op)) {
1441 return failure();
1442 }
1443 SymbolTableCollection tables;
1444 auto lookupRes = lookupTopLevelSymbol<FuncDefOp>(tables, op.getCalleeAttr(), op);
1445 if (failed(lookupRes)) {
1446 return failure();
1447 }
1448 if (failed(instantiateViaTargetType(in, out, callArgTypes, lookupRes->get()))) {
1449 return failure();
1450 }
1451 LLVM_DEBUG({
1452 llvm::dbgs() << "[InstantiateAtCallOpCompute] propagated instantiations via symrefs in "
1453 "result type params: "
1454 << debug::toStringList(out.paramsOfStructTy) << '\n';
1455 });
1456 }
1457
1458 StructType newRetTy = StructType::get(oldRetTy.getNameRef(), out.paramsOfStructTy);
1459 LLVM_DEBUG(llvm::dbgs() << "[InstantiateAtCallOpCompute] newRetTy: " << newRetTy << '\n');
1460 if (newRetTy == oldRetTy) {
1461 return failure(); // nothing changed
1462 }
1463 // The `newRetTy` is computed via instantiateViaTargetType() which can only preserve the
1464 // original Attribute or convert to a concrete attribute via the unification process. Thus, if
1465 // the conversion here is illegal it means there is a type conflict within the LLZK code that
1466 // prevents instantiation of the struct with the requested type.
1467 if (!tracker_.isLegalConversion(oldRetTy, newRetTy, "InstantiateAtCallOpCompute")) {
1468 return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) {
1469 diag.append(
1470 "result type mismatch: due to struct instantiation, expected type ", newRetTy,
1471 ", but found ", oldRetTy
1472 );
1473 });
1474 }
1475 LLVM_DEBUG(llvm::dbgs() << "[InstantiateAtCallOpCompute] replaced " << op);
1477 rewriter, op, TypeRange {newRetTy}, op.getCallee(),
1478 AffineMapFolder::getConvertedMapOpGroups(out), out.dimsPerGroup, op.getArgOperands()
1479 );
1480 (void)newOp; // tell compiler it's intentionally unused in release builds
1481 LLVM_DEBUG(llvm::dbgs() << " with " << newOp << '\n');
1482 return success();
1483 }
1484
1485private:
1488 inline LogicalResult instantiateViaTargetType(
1489 const AffineMapFolder::Input &in, AffineMapFolder::Output &out,
1490 OperandRange::type_range callArgTypes, FuncDefOp targetFunc
1491 ) const {
1492 assert(targetFunc.isStructCompute()); // since `op.calleeIsStructCompute()`
1493 ArrayAttr targetResTyParams = targetFunc.getSingleResultTypeOfCompute().getParams();
1494 assert(!isNullOrEmpty(targetResTyParams)); // same cardinality as `in.paramsOfStructTy`
1495 assert(in.paramsOfStructTy.size() == targetResTyParams.size()); // verifier ensures this
1496
1497 if (llvm::all_of(in.paramsOfStructTy, isConcreteAttr<>)) {
1498 // Nothing can change if everything is already concrete
1499 return failure();
1500 }
1501
1502 LLVM_DEBUG({
1503 llvm::dbgs() << '[' << __FUNCTION__ << ']'
1504 << " call arg types: " << debug::toStringList(callArgTypes) << '\n';
1505 llvm::dbgs() << '[' << __FUNCTION__ << ']' << " target func arg types: "
1506 << debug::toStringList(targetFunc.getArgumentTypes()) << '\n';
1507 llvm::dbgs() << '[' << __FUNCTION__ << ']'
1508 << " struct params @ call: " << debug::toStringList(in.paramsOfStructTy) << '\n';
1509 llvm::dbgs() << '[' << __FUNCTION__ << ']'
1510 << " target struct params: " << debug::toStringList(targetResTyParams) << '\n';
1511 });
1512
1513 UnificationMap unifications;
1514 bool unifies = typeListsUnify(targetFunc.getArgumentTypes(), callArgTypes, {}, &unifications);
1515 (void)unifies; // tell compiler it's intentionally unused in builds without assertions
1516 assert(unifies && "should have been checked by verifiers");
1517
1518 LLVM_DEBUG({
1519 llvm::dbgs() << '[' << __FUNCTION__ << ']'
1520 << " unifications of arg types: " << debug::toStringList(unifications) << '\n';
1521 });
1522
1523 // Check for LHS SymRef (i.e., from the target function) that have RHS concrete Attributes (i.e.
1524 // from the call argument types) without any struct parameters (because the type with concrete
1525 // struct parameters will be used to instantiate the target struct rather than the fully
1526 // flattened struct type resulting in type mismatch of the callee to target) and perform those
1527 // replacements in the `targetFunc` return type to produce the new result type for the CallOp.
1528 SmallVector<Attribute> newReturnStructParams = llvm::map_to_vector(
1529 llvm::zip_equal(targetResTyParams.getValue(), in.paramsOfStructTy),
1530 [&unifications](std::tuple<Attribute, Attribute> p) {
1531 Attribute fromCall = std::get<1>(p);
1532 // Preserve attributes that are already concrete at the call site. Otherwise attempt to lookup
1533 // non-parameterized concrete unification for the target struct parameter symbol.
1534 if (!isConcreteAttr(fromCall)) {
1535 Attribute fromTgt = std::get<0>(p);
1536 LLVM_DEBUG({
1537 llvm::dbgs() << "[instantiateViaTargetType] fromCall = " << fromCall << '\n';
1538 llvm::dbgs() << "[instantiateViaTargetType] fromTgt = " << fromTgt << '\n';
1539 });
1540 assert(llvm::isa<SymbolRefAttr>(fromTgt));
1541 auto it = unifications.find(std::make_pair(llvm::cast<SymbolRefAttr>(fromTgt), Side::LHS));
1542 if (it != unifications.end()) {
1543 Attribute unifiedAttr = it->second;
1544 LLVM_DEBUG({
1545 llvm::dbgs() << "[instantiateViaTargetType] unifiedAttr = " << unifiedAttr << '\n';
1546 });
1547 if (unifiedAttr && isConcreteAttr<false>(unifiedAttr)) {
1548 return unifiedAttr;
1549 }
1550 }
1551 }
1552 return fromCall;
1553 }
1554 );
1555
1556 out.paramsOfStructTy = std::move(newReturnStructParams);
1557 assert(out.paramsOfStructTy.size() == in.paramsOfStructTy.size() && "post-condition");
1558 assert(out.mapOpGroups.empty() && "post-condition");
1559 assert(out.dimsPerGroup.empty() && "post-condition");
1560 return success();
1561 }
1562};
1563
1564LogicalResult run(ModuleOp modOp, ConversionTracker &tracker) {
1565 MLIRContext *ctx = modOp.getContext();
1566 RewritePatternSet patterns(ctx);
1567 patterns.add<
1568 InstantiateAtCreateArrayOp, // CreateArrayOp
1569 InstantiateAtCallOpCompute // CallOp, targeting struct "compute()"
1570 >(ctx, tracker);
1571
1572 return applyAndFoldGreedily(modOp, tracker, std::move(patterns));
1573}
1574
1575} // namespace Step4_InstantiateAffineMaps
1576
1578
1580class UpdateNewArrayElemFromWrite final : public OpRewritePattern<CreateArrayOp> {
1581 ConversionTracker &tracker_;
1582
1583public:
1584 UpdateNewArrayElemFromWrite(MLIRContext *ctx, ConversionTracker &tracker)
1585 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
1586
1587 LogicalResult matchAndRewrite(CreateArrayOp op, PatternRewriter &rewriter) const override {
1588 Value createResult = op.getResult();
1589 ArrayType createResultType = dyn_cast<ArrayType>(createResult.getType());
1590 assert(createResultType && "CreateArrayOp must produce ArrayType");
1591 Type oldResultElemType = createResultType.getElementType();
1592
1593 // Look for WriteArrayOp where the array reference is the result of the CreateArrayOp and the
1594 // element type is different.
1595 Type newResultElemType = nullptr;
1596 for (Operation *user : createResult.getUsers()) {
1597 if (WriteArrayOp writeOp = dyn_cast<WriteArrayOp>(user)) {
1598 if (writeOp.getArrRef() != createResult) {
1599 continue;
1600 }
1601 Type writeRValueType = writeOp.getRvalue().getType();
1602 if (writeRValueType == oldResultElemType) {
1603 continue;
1604 }
1605 if (newResultElemType && newResultElemType != writeRValueType) {
1606 LLVM_DEBUG(
1607 llvm::dbgs()
1608 << "[UpdateNewArrayElemFromWrite] multiple possible element types for CreateArrayOp "
1609 << newResultElemType << " vs " << writeRValueType << '\n'
1610 );
1611 return failure();
1612 }
1613 newResultElemType = writeRValueType;
1614 }
1615 }
1616 if (!newResultElemType) {
1617 // no replacement type found
1618 return failure();
1619 }
1620 if (!tracker_.isLegalConversion(
1621 oldResultElemType, newResultElemType, "UpdateNewArrayElemFromWrite"
1622 )) {
1623 return failure();
1624 }
1625 ArrayType newType = createResultType.cloneWith(newResultElemType);
1626 rewriter.modifyOpInPlace(op, [&createResult, &newType]() { createResult.setType(newType); });
1627 LLVM_DEBUG(
1628 llvm::dbgs() << "[UpdateNewArrayElemFromWrite] updated result type of " << op << '\n'
1629 );
1630 return success();
1631 }
1632};
1633
1634namespace {
1635
1636LogicalResult updateArrayElemFromArrAccessOp(
1637 ArrayAccessOpInterface op, Type scalarElemTy, ConversionTracker &tracker,
1638 PatternRewriter &rewriter
1639) {
1640 ArrayType oldArrType = op.getArrRefType();
1641 if (oldArrType.getElementType() == scalarElemTy) {
1642 return failure(); // no change needed
1643 }
1644 ArrayType newArrType = oldArrType.cloneWith(scalarElemTy);
1645 if (oldArrType == newArrType ||
1646 !tracker.isLegalConversion(oldArrType, newArrType, "updateArrayElemFromArrAccessOp")) {
1647 return failure();
1648 }
1649 rewriter.modifyOpInPlace(op, [&op, &newArrType]() { op.getArrRef().setType(newArrType); });
1650 LLVM_DEBUG(
1651 llvm::dbgs() << "[updateArrayElemFromArrAccessOp] updated base array type in " << op << '\n'
1652 );
1653 return success();
1654}
1655
1656} // namespace
1657
1658class UpdateArrayElemFromArrWrite final : public OpRewritePattern<WriteArrayOp> {
1659 ConversionTracker &tracker_;
1660
1661public:
1662 UpdateArrayElemFromArrWrite(MLIRContext *ctx, ConversionTracker &tracker)
1663 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
1664
1665 LogicalResult matchAndRewrite(WriteArrayOp op, PatternRewriter &rewriter) const override {
1666 return updateArrayElemFromArrAccessOp(op, op.getRvalue().getType(), tracker_, rewriter);
1667 }
1668};
1669
1670class UpdateArrayElemFromArrRead final : public OpRewritePattern<ReadArrayOp> {
1671 ConversionTracker &tracker_;
1672
1673public:
1674 UpdateArrayElemFromArrRead(MLIRContext *ctx, ConversionTracker &tracker)
1675 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
1676
1677 LogicalResult matchAndRewrite(ReadArrayOp op, PatternRewriter &rewriter) const override {
1678 return updateArrayElemFromArrAccessOp(op, op.getResult().getType(), tracker_, rewriter);
1679 }
1680};
1681
1685static bool isFullySpecializedMemberType(Type type) {
1686 if (!isConcreteType(type, /*allowStructParams=*/false)) {
1687 return false;
1688 }
1689 return !type.walk([](ArrayType array) {
1690 for (Attribute dim : array.getDimensionSizes()) {
1691 if (auto integer = dyn_cast<IntegerAttr>(dim); integer && isDynamic(integer)) {
1692 return WalkResult::interrupt();
1693 }
1694 }
1695 return WalkResult::advance();
1696 }).wasInterrupted();
1697}
1698
1700class UpdateMemberDefTypeFromWrite final : public OpRewritePattern<MemberDefOp> {
1701 ConversionTracker &tracker_;
1702
1703public:
1704 UpdateMemberDefTypeFromWrite(MLIRContext *ctx, ConversionTracker &tracker)
1705 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
1706
1707 LogicalResult matchAndRewrite(MemberDefOp op, PatternRewriter &rewriter) const override {
1708 // Avoid a whole-struct symbol-use search when writes cannot refine the declaration.
1709 if (isFullySpecializedMemberType(op.getType())) {
1710 return failure();
1711 }
1712
1713 // Find all uses of the member symbol name within its parent struct.
1715 assert(parentRes && "MemberDefOp parent is always StructDefOp"); // per ODS def
1716
1717 // If the symbol is used by a MemberWriteOp with a different result type then change
1718 // the type of the MemberDefOp to match the MemberWriteOp result type.
1719 Type newType = nullptr;
1720 if (auto memberUsers = llzk::getSymbolUses(op, parentRes)) {
1721 std::optional<Location> newTypeLoc = std::nullopt;
1722 for (SymbolTable::SymbolUse symUse : memberUsers.value()) {
1723 if (MemberWriteOp writeOp = llvm::dyn_cast<MemberWriteOp>(symUse.getUser())) {
1724 Type writeToType = writeOp.getVal().getType();
1725 LLVM_DEBUG(llvm::dbgs() << "[UpdateMemberDefTypeFromWrite] checking " << writeOp << '\n');
1726 if (!newType) {
1727 // If a new type has not yet been discovered, store the new type.
1728 newType = writeToType;
1729 newTypeLoc = writeOp.getLoc();
1730 } else if (writeToType != newType) {
1731 // Typically, there will only be one write for each member of a struct but do not rely
1732 // on that assumption. If multiple writes with a different types A and B are found where
1733 // A->B is a legal conversion (i.e., more concrete unification), then it is safe to use
1734 // type B with the assumption that the write with type A will be updated by another
1735 // pattern to also use type B.
1736 if (!tracker_.isLegalConversion(writeToType, newType, "UpdateMemberDefTypeFromWrite")) {
1737 if (tracker_.isLegalConversion(
1738 newType, writeToType, "UpdateMemberDefTypeFromWrite"
1739 )) {
1740 // 'writeToType' is the more concrete type
1741 newType = writeToType;
1742 newTypeLoc = writeOp.getLoc();
1743 } else {
1744 // Give an error if the types are incompatible.
1745 return rewriter.notifyMatchFailure(op, [&](Diagnostic &diag) {
1746 diag.append(
1747 "Cannot update type of '", MemberDefOp::getOperationName(),
1748 "' because there are multiple '", MemberWriteOp::getOperationName(),
1749 "' with different value types"
1750 );
1751 if (newTypeLoc) {
1752 diag.attachNote(newTypeLoc).append("type written here is ", newType);
1753 }
1754 diag.attachNote(writeOp.getLoc()).append("type written here is ", writeToType);
1755 });
1756 }
1757 }
1758 }
1759 }
1760 }
1761 }
1762 if (!newType || newType == op.getType()) {
1763 return failure(); // nothing changed
1764 }
1765 if (!tracker_.isLegalConversion(op.getType(), newType, "UpdateMemberDefTypeFromWrite")) {
1766 return failure();
1767 }
1768 rewriter.modifyOpInPlace(op, [&op, &newType]() { op.setType(newType); });
1769 LLVM_DEBUG(llvm::dbgs() << "[UpdateMemberDefTypeFromWrite] updated type of " << op << '\n');
1770 return success();
1771 }
1772};
1773
1774namespace {
1775
1776SmallVector<std::unique_ptr<Region>> moveRegions(Operation *op) {
1777 SmallVector<std::unique_ptr<Region>> newRegions;
1778 for (Region &region : op->getRegions()) {
1779 auto newRegion = std::make_unique<Region>();
1780 newRegion->takeBody(region);
1781 newRegions.push_back(std::move(newRegion));
1782 }
1783 return newRegions;
1784}
1785
1786} // namespace
1787
1790class UpdateInferredResultTypes final : public OpTraitRewritePattern<OpTrait::InferTypeOpAdaptor> {
1791 ConversionTracker &tracker_;
1792
1793public:
1794 UpdateInferredResultTypes(MLIRContext *ctx, ConversionTracker &tracker)
1795 : OpTraitRewritePattern(ctx, 6), tracker_(tracker) {}
1796
1797 LogicalResult matchAndRewrite(Operation *op, PatternRewriter &rewriter) const override {
1798 SmallVector<Type, 1> inferredResultTypes;
1799 InferTypeOpInterface retTypeFn = llvm::cast<InferTypeOpInterface>(op);
1800 LogicalResult result = retTypeFn.inferReturnTypes(
1801 op->getContext(), op->getLoc(), op->getOperands(), op->getRawDictionaryAttrs(),
1802 op->getPropertiesStorage(), op->getRegions(), inferredResultTypes
1803 );
1804 if (failed(result)) {
1805 return failure();
1806 }
1807 if (op->getResultTypes() == inferredResultTypes) {
1808 return failure(); // nothing changed
1809 }
1810 if (!tracker_.areLegalConversions(
1811 op->getResultTypes(), inferredResultTypes, "UpdateInferredResultTypes"
1812 )) {
1813 return failure();
1814 }
1815
1816 // Move nested region bodies and replace the original op with the updated types list.
1817 LLVM_DEBUG(llvm::dbgs() << "[UpdateInferredResultTypes] replaced " << *op);
1818 SmallVector<std::unique_ptr<Region>> newRegions = moveRegions(op);
1819 Operation *newOp = rewriter.create(
1820 op->getLoc(), op->getName().getIdentifier(), op->getOperands(), inferredResultTypes,
1821 op->getAttrs(), op->getSuccessors(), newRegions
1822 );
1823 rewriter.replaceOp(op, newOp);
1824 LLVM_DEBUG(llvm::dbgs() << " with " << *newOp << '\n');
1825 return success();
1826 }
1827};
1828
1830class UpdateFuncTypeFromReturn final : public OpRewritePattern<FuncDefOp> {
1831 ConversionTracker &tracker_;
1832
1833public:
1834 UpdateFuncTypeFromReturn(MLIRContext *ctx, ConversionTracker &tracker)
1835 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
1836
1837 LogicalResult matchAndRewrite(FuncDefOp op, PatternRewriter &rewriter) const override {
1838 Region &body = op.getFunctionBody();
1839 if (body.empty()) {
1840 return failure();
1841 }
1842 ReturnOp retOp = llvm::dyn_cast<ReturnOp>(body.back().getTerminator());
1843 assert(retOp && "final op in body region must be return");
1844 OperandRange::type_range tyFromReturnOp = retOp.getOperands().getTypes();
1845
1846 FunctionType oldFuncTy = op.getFunctionType();
1847 if (oldFuncTy.getResults() == tyFromReturnOp) {
1848 return failure(); // nothing changed
1849 }
1850 if (!tracker_.areLegalConversions(
1851 oldFuncTy.getResults(), tyFromReturnOp, "UpdateFuncTypeFromReturn"
1852 )) {
1853 return failure();
1854 }
1855
1856 rewriter.modifyOpInPlace(op, [&]() {
1857 op.setFunctionType(rewriter.getFunctionType(oldFuncTy.getInputs(), tyFromReturnOp));
1858 });
1859 LLVM_DEBUG(
1860 llvm::dbgs() << "[UpdateFuncTypeFromReturn] changed " << op.getSymName() << " from "
1861 << oldFuncTy << " to " << op.getFunctionType() << '\n'
1862 );
1863 return success();
1864 }
1865};
1866
1871class UpdateFreeFuncCallOpTypes final : public OpRewritePattern<CallOp> {
1872 ConversionTracker &tracker_;
1873
1874public:
1875 UpdateFreeFuncCallOpTypes(MLIRContext *ctx, ConversionTracker &tracker)
1876 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
1877
1878 LogicalResult matchAndRewrite(CallOp op, PatternRewriter &rewriter) const override {
1879 if (calleeReferencesTemplateParam(op)) {
1880 return failure();
1881 }
1882 SymbolTableCollection tables;
1883 auto lookupRes = lookupTopLevelSymbol<FuncDefOp>(tables, op.getCalleeAttr(), op);
1884 if (failed(lookupRes)) {
1885 return failure();
1886 }
1887 FuncDefOp targetFunc = lookupRes->get();
1888 if (targetFunc.isInStruct()) {
1889 // this pattern only applies when the callee is NOT in a struct
1890 return failure();
1891 }
1892 if (op.getResultTypes() == targetFunc.getFunctionType().getResults()) {
1893 return failure(); // nothing changed
1894 }
1895 if (!tracker_.areLegalConversions(
1896 op.getResultTypes(), targetFunc.getFunctionType().getResults(),
1897 "UpdateFreeFuncCallOpTypes"
1898 )) {
1899 return failure();
1900 }
1901
1902 LLVM_DEBUG(llvm::dbgs() << "[UpdateFreeFuncCallOpTypes] replaced " << op);
1903 CallOp newOp = replaceOpWithNewOp<CallOp>(rewriter, op, targetFunc, op.getArgOperands());
1904 (void)newOp; // tell compiler it's intentionally unused in release builds
1905 LLVM_DEBUG(llvm::dbgs() << " with " << newOp << '\n');
1906 return success();
1907 }
1908};
1909
1910namespace {
1911
1912LogicalResult updateMemberRefValFromMemberDef(
1913 MemberRefOpInterface op, ConversionTracker &tracker, PatternRewriter &rewriter
1914) {
1915 SymbolTableCollection tables;
1916 auto def = op.getMemberDefOp(tables);
1917 if (failed(def)) {
1918 return failure();
1919 }
1920 Type oldResultType = op.getVal().getType();
1921 Type newResultType = def->get().getType();
1922 if (oldResultType == newResultType ||
1923 !tracker.isLegalConversion(oldResultType, newResultType, "updateMemberRefValFromMemberDef")) {
1924 return failure();
1925 }
1926 rewriter.modifyOpInPlace(op, [&op, &newResultType]() { op.getVal().setType(newResultType); });
1927 LLVM_DEBUG(
1928 llvm::dbgs() << "[updateMemberRefValFromMemberDef] updated value type in " << op << '\n'
1929 );
1930 return success();
1931}
1932
1933} // namespace
1934
1936class UpdateMemberReadValFromDef final : public OpRewritePattern<MemberReadOp> {
1937 ConversionTracker &tracker_;
1938
1939public:
1940 UpdateMemberReadValFromDef(MLIRContext *ctx, ConversionTracker &tracker)
1941 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
1942
1943 LogicalResult matchAndRewrite(MemberReadOp op, PatternRewriter &rewriter) const override {
1944 return updateMemberRefValFromMemberDef(op, tracker_, rewriter);
1945 }
1946};
1947
1949class UpdateMemberWriteValFromDef final : public OpRewritePattern<MemberWriteOp> {
1950 ConversionTracker &tracker_;
1951
1952public:
1953 UpdateMemberWriteValFromDef(MLIRContext *ctx, ConversionTracker &tracker)
1954 : OpRewritePattern(ctx, 3), tracker_(tracker) {}
1955
1956 LogicalResult matchAndRewrite(MemberWriteOp op, PatternRewriter &rewriter) const override {
1957 return updateMemberRefValFromMemberDef(op, tracker_, rewriter);
1958 }
1959};
1960
1961LogicalResult run(ModuleOp modOp, ConversionTracker &tracker) {
1962 MLIRContext *ctx = modOp.getContext();
1963 RewritePatternSet patterns(ctx);
1964 patterns.add<
1965 // Benefit of this one must be higher than rules that would propagate the type in the opposite
1966 // direction (ex: `UpdateArrayElemFromArrRead`) else the greedy conversion would not converge.
1967 // benefit = 6
1968 UpdateInferredResultTypes, // OpTrait::InferTypeOpAdaptor (ReadArrayOp, ExtractArrayOp)
1969 // benefit = 3
1970 UpdateFreeFuncCallOpTypes, // CallOp, targeting non-struct functions
1971 UpdateFuncTypeFromReturn, // FuncDefOp
1972 UpdateNewArrayElemFromWrite, // CreateArrayOp
1973 UpdateArrayElemFromArrRead, // ReadArrayOp
1974 UpdateArrayElemFromArrWrite, // WriteArrayOp
1975 UpdateMemberDefTypeFromWrite, // MemberDefOp
1976 UpdateMemberReadValFromDef, // MemberReadOp
1977 UpdateMemberWriteValFromDef // MemberWriteOp
1978 >(ctx, tracker);
1979
1980 return applyAndFoldGreedily(modOp, tracker, std::move(patterns));
1981}
1982} // namespace Step5_PropagateTypes
1983
1984namespace Step6_Cleanup {
1985
1986struct FromKeepSet : public CleanupBase {
1988
1991 static bool hasTemplateSymbolBindings(Operation *op) {
1992 if (StructDefOp sdef = llvm::dyn_cast<StructDefOp>(op)) {
1993 return sdef.hasTemplateSymbolBindings();
1994 }
1995 if (llvm::isa<function::FuncDefOp>(op)) {
1996 if (TemplateOp parent = getParentOfType<TemplateOp>(op)) {
1997 return parent.hasConstOps<TemplateSymbolBindingOpInterface>();
1998 }
1999 }
2000 return false;
2001 }
2002
2006 LogicalResult eraseUnreachableFrom(ArrayRef<SymbolOpInterface> keep) {
2007 // Initialize roots from the given symbol definitions.
2008 SetVector<SymbolOpInterface> roots(keep.begin(), keep.end());
2009 // Add GlobalDefOp to the set of roots.
2010 rootMod.walk([&roots](global::GlobalDefOp gdef) { roots.insert(gdef); });
2011
2012 // Use a SymbolDefTree to find all Symbol defs reachable from one of the root nodes. Then
2013 // collect all Symbol uses reachable from those def nodes. These are the symbols that should
2014 // be preserved. All other symbol defs should be removed.
2015 DenseSet<Operation *> defsToKeep;
2016 llvm::df_iterator_default_set<const SymbolUseGraphNode *> symbolsToKeep;
2017 for (size_t i = 0; i < roots.size(); ++i) { // iterate for safe insertion
2018 SymbolOpInterface keepRoot = roots[i];
2019 LLVM_DEBUG({ llvm::dbgs() << "[EraseUnreachable] root: " << keepRoot << '\n'; });
2020 const SymbolDefTreeNode *keepRootNode = defTree.lookupNode(keepRoot);
2021 assert(keepRootNode && "every symbol def must be in the def tree");
2022 for (const SymbolDefTreeNode *reachableDefNode : llvm::depth_first(keepRootNode)) {
2023 LLVM_DEBUG({
2024 llvm::dbgs() << "[EraseUnreachable] can reach: " << reachableDefNode->getOp() << '\n';
2025 });
2026 if (SymbolOpInterface reachableDef = reachableDefNode->getOp()) {
2027 if (isErasableDefinition(reachableDef.getOperation())) {
2028 defsToKeep.insert(reachableDef.getOperation());
2029 }
2030 // Use 'depth_first_ext()' to get all symbol uses reachable from the current Symbol def
2031 // node. There are no uses if the node is not in the graph. Within the loop that populates
2032 // 'depth_first_ext()', also check if the symbol is an erasable definition and ensure it
2033 // is in 'roots' so the outer loop preserves all symbols reachable from it.
2034 if (const SymbolUseGraphNode *useGraphNodeForDef = useGraph.lookupNode(reachableDef)) {
2035 for (const SymbolUseGraphNode *usedSymbolNode :
2036 depth_first_ext(useGraphNodeForDef, symbolsToKeep)) {
2037 LLVM_DEBUG({
2038 llvm::dbgs() << "[EraseUnreachable] uses symbol: "
2039 << usedSymbolNode->getSymbolPath() << '\n';
2040 });
2041 // Ignore struct/template parameter symbols (before doing the lookup below because it
2042 // would fail anyway and then cause the "failed" case to be triggered unnecessarily).
2043 if (usedSymbolNode->isTemplateSymbolBinding()) {
2044 continue;
2045 }
2046 // If `usedSymbolNode` references an erasable definition, ensure it's considered in
2047 // the roots so symbols reachable from its body are preserved too.
2048 auto lookupRes = usedSymbolNode->lookupSymbol(tables);
2049 if (failed(lookupRes)) {
2050 LLVM_DEBUG(useGraph.dumpToDotFile());
2051 return failure();
2052 }
2053 // If loaded via an IncludeOp it's not in the current AST anyway so ignore.
2054 if (lookupRes->viaInclude()) {
2055 continue;
2056 }
2057 Operation *usedOp = lookupRes->get();
2058 if (isErasableDefinition(usedOp)) {
2059 SymbolOpInterface asSymbol = llvm::cast<SymbolOpInterface>(usedOp);
2060 bool insertRes = roots.insert(asSymbol);
2061 (void)insertRes; // tell compiler it's intentionally unused in release builds
2062 LLVM_DEBUG({
2063 if (insertRes) {
2064 llvm::dbgs() << "[EraseUnreachable] found another root: " << asSymbol << '\n';
2065 }
2066 });
2067 }
2068 }
2069 }
2070 }
2071 }
2072 }
2073
2074 SmallVector<SymbolOpInterface> toErase;
2075 rootMod.walk([this, &defsToKeep, &symbolsToKeep, &toErase](Operation *op) {
2076 if (!isErasableDefinition(op) || defsToKeep.contains(op)) {
2077 return;
2078 }
2079 SymbolOpInterface symOp = llvm::cast<SymbolOpInterface>(op);
2080 const SymbolUseGraphNode *n = this->useGraph.lookupNode(symOp);
2081 if (!n || !symbolsToKeep.contains(n)) {
2082 LLVM_DEBUG(llvm::dbgs() << "[EraseUnreachable] removing: " << symOp.getNameAttr() << '\n');
2083 toErase.push_back(symOp);
2084 }
2085 });
2086 for (SymbolOpInterface symOp : toErase) {
2087 symOp.erase();
2088 }
2089
2090 return success();
2091 }
2092};
2093
2094} // namespace Step6_Cleanup
2095
2096class PassImpl : public llzk::polymorphic::impl::FlatteningPassBase<PassImpl> {
2097 using Base = FlatteningPassBase<PassImpl>;
2098 using Base::Base;
2099
2101 FlatteningCleanupMode getEffectiveCleanupMode() const {
2102 FlatteningCleanupMode m = cleanupMode.getValue();
2104 }
2105
2106 void runOnOperation() override {
2107 ModuleOp modOp = getOperation();
2108 if (failed(runOn(modOp))) {
2109 LLVM_DEBUG({
2110 // If the pass failed, dump the current IR.
2111 llvm::dbgs() << "=====================================================================\n";
2112 llvm::dbgs() << " Dumping module after failure of pass " << DEBUG_TYPE << '\n';
2113 modOp.print(llvm::dbgs(), OpPrintingFlags().assumeVerified());
2114 llvm::dbgs() << "=====================================================================\n";
2115 });
2116 signalPassFailure();
2117 }
2118 }
2119
2120 inline LogicalResult runOn(ModuleOp modOp) {
2121 FlatteningCleanupMode effectiveCleanupMode = getEffectiveCleanupMode();
2122 // If the cleanup mode is set to remove anything not reachable from the main struct, do an
2123 // initial pass to remove things that are not reachable (as an optimization) because creating
2124 // an instantiated version of a struct will not cause something to become reachable that was
2125 // not already reachable in parameterized form.
2126 if (effectiveCleanupMode == FlatteningCleanupMode::MainAsRoot) {
2127 if (failed(eraseUnreachableFromMainStruct(modOp))) {
2128 return failure();
2129 }
2130 }
2131
2132 // Pass Manager to run some standard cleanup passes that are always beneficial:
2133 // - Remove templates that contain no struct or function definitions
2134 // - Convert templates with no constant parameters or expressions into modules
2135 OpPassManager universalCleanup(ModuleOp::getOperationName());
2136 universalCleanup.addPass(createEmptyTemplateRemovalPass());
2137
2138 // Run universal cleanup as a preliminary step to satisfy the
2139 // `assert(!isNullOrEmpty(paramNames))` precondition in `genClone()`.
2140 if (failed(runPipeline(universalCleanup, modOp))) {
2141 return failure();
2142 }
2143
2144 ConversionTracker tracker;
2145 if (failed(Step1_InstantiateStructs::instantiateMainStruct(modOp, tracker))) {
2146 llvm::errs() << DEBUG_TYPE << " failed while instantiating the main struct\n";
2147 return failure();
2148 }
2149
2150 unsigned loopCount = 0;
2151 do {
2152 ++loopCount;
2153 if (loopCount > iterationLimit) {
2154 llvm::errs() << DEBUG_TYPE << " exceeded the limit of " << iterationLimit
2155 << " iterations!\n";
2156 return failure();
2157 }
2158 tracker.resetModifiedFlag();
2159
2160 LLVM_DEBUG({
2161 llvm::dbgs() << "[FlatteningPass(count=" << loopCount
2162 << ")] Running step 1: struct instantiation\n";
2163 });
2164 // Find calls to "compute()" that return a parameterized struct type and replace it to call an
2165 // instantiated version of the struct that has parameters replaced with the constant values.
2166 // Create the necessary instantiated/flattened struct in the same location as the original.
2167 if (failed(Step1_InstantiateStructs::run(modOp, tracker))) {
2168 llvm::errs() << DEBUG_TYPE << " failed while instantiating structs in templates\n";
2169 return failure();
2170 }
2171 // Instantiate calls to templated functions.
2172 if (failed(Step2_InstantiateFunctions::run(modOp, tracker))) {
2173 llvm::errs() << DEBUG_TYPE << " failed while instantiating functions in templates\n";
2174 return failure();
2175 }
2176
2177 LLVM_DEBUG({
2178 llvm::dbgs() << "[FlatteningPass(count=" << loopCount
2179 << ")] Running step 2: loop unrolling\n";
2180 });
2181 // Unroll loops with known iterations.
2182 if (failed(Step3_Unroll::run(modOp, tracker))) {
2183 llvm::errs() << DEBUG_TYPE << " failed while unrolling loops\n";
2184 return failure();
2185 }
2186
2187 LLVM_DEBUG({
2188 llvm::dbgs() << "[FlatteningPass(count=" << loopCount
2189 << ")] Running step 3: affine maps instantiation\n";
2190 });
2191 // Instantiate affine_map parameters of StructType and ArrayType.
2192 if (failed(Step4_InstantiateAffineMaps::run(modOp, tracker))) {
2193 llvm::errs() << DEBUG_TYPE << " failed while instantiating `affine_map` parameters\n";
2194 return failure();
2195 }
2196
2197 LLVM_DEBUG({
2198 llvm::dbgs() << "[FlatteningPass(count=" << loopCount
2199 << ")] Running step 4: type propagation\n";
2200 });
2201 // Propagate updated types using the semantics of various ops.
2202 if (failed(Step5_PropagateTypes::run(modOp, tracker))) {
2203 llvm::errs() << DEBUG_TYPE << " failed while propagating instantiated types\n";
2204 return failure();
2205 }
2206
2207 LLVM_DEBUG(if (tracker.isModified()) {
2208 llvm::dbgs() << "=====================================================================\n";
2209 llvm::dbgs() << " Dumping module between iterations of " << DEBUG_TYPE << '\n';
2210 modOp.print(llvm::dbgs(), OpPrintingFlags().assumeVerified());
2211 llvm::dbgs() << "=====================================================================\n";
2212 });
2213 } while (tracker.isModified());
2214
2215 tracker.clearPartialFuncInstantiations();
2216
2217 // Run user-selected cleanup first.
2218 if (failed(cleanupSwitch(modOp, tracker))) {
2219 return failure();
2220 }
2221 // Run universal cleanup again since no-param or param-only structs may exist now.
2222 if (failed(runPipeline(universalCleanup, modOp))) {
2223 return failure();
2224 }
2225
2226 OpPassManager allocationCleanup(ModuleOp::getOperationName());
2227 allocationCleanup.addPass(createRemoveUnusedDiscardableAllocationsPass(
2228 RemoveUnusedDiscardableAllocationsPassOptions {
2229 .allocatorOpName = CreateArrayOp::getOperationName().str()
2230 }
2231 ));
2232 return runPipeline(allocationCleanup, modOp);
2233 }
2234
2235 // Perform cleanup according to the 'cleanupMode' option.
2236 LogicalResult cleanupSwitch(ModuleOp modOp, const ConversionTracker &tracker) {
2237 FlatteningCleanupMode effectiveCleanupMode = getEffectiveCleanupMode();
2238 LLVM_DEBUG({ llvm::dbgs() << "[FlatteningPass] Running step 5: cleanup "; });
2239 switch (effectiveCleanupMode) {
2240 case FlatteningCleanupMode::MainAsRoot:
2241 LLVM_DEBUG(llvm::dbgs() << "(main as root mode)\n");
2242 return eraseUnreachableFromMainStruct(modOp, false);
2243 case FlatteningCleanupMode::ConcreteAsRoot:
2244 LLVM_DEBUG(llvm::dbgs() << "(concrete definitions mode)\n");
2245 return eraseUnreachableFromConcreteDefinitions(modOp);
2246 case FlatteningCleanupMode::Preimage:
2247 LLVM_DEBUG(llvm::dbgs() << "(preimage mode)\n");
2248 return erasePreimageOfInstantiations(modOp, tracker);
2249 case FlatteningCleanupMode::Unspecified:
2250 default:
2251 LLVM_DEBUG(llvm::dbgs() << "(disabled)\n");
2252 return success();
2253 }
2254 }
2255
2256 // Erase parameterized definitions that were replaced with concrete instantiations.
2257 LogicalResult erasePreimageOfInstantiations(ModuleOp rootMod, const ConversionTracker &tracker) {
2258 // TODO: The names from getInstantiatedDefinitionNames() are NOT guaranteed to be paths from the
2259 // "top root" and they also do not indicate a root module so there could be ambiguity. This is a
2260 // broader problem in the FlatteningPass itself so let's just assume, for now, that these are
2261 // paths from the "top root". See [LLZK-286].
2262 FromEraseSet cleaner(
2263 rootMod, getAnalysis<SymbolDefTree>(), getAnalysis<SymbolUseGraph>(),
2264 tracker.getInstantiatedDefinitionNames()
2265 );
2266 LogicalResult res = cleaner.eraseUnusedDefinitions();
2267 if (succeeded(res)) {
2268 LLVM_DEBUG(llvm::dbgs() << "[Cleanup(preimage)] success\n";);
2269 // Warn about any definitions that were instantiated but still have uses elsewhere.
2270 const SymbolUseGraph *useGraph = nullptr;
2271 rootMod->walk([this, &cleaner, &useGraph](Operation *walkedOp) {
2272 SymbolOpInterface op = llvm::dyn_cast<SymbolOpInterface>(walkedOp);
2273 if (!op || !cleaner.getTryToEraseSet().contains(op)) {
2274 return;
2275 }
2276 // If needed, rebuild use graph to reflect deletions.
2277 if (!useGraph) {
2278 useGraph = &getAnalysis<SymbolUseGraph>();
2279 }
2280 // If the op has any users, report the warning.
2281 if (useGraph->lookupNode(op)->hasPredecessor()) {
2282 op.emitWarning("Parameterized definition still has uses!").report();
2283 }
2284 });
2285 } else {
2286 LLVM_DEBUG(llvm::dbgs() << "[Cleanup(preimage)] failed\n";);
2287 }
2288 return res;
2289 }
2290
2291 LogicalResult eraseUnreachableFromConcreteDefinitions(ModuleOp rootMod) {
2292 SmallVector<SymbolOpInterface> roots;
2293 rootMod.walk([&roots](Operation *op) {
2294 if (isErasableDefinition(op) && !Step6_Cleanup::FromKeepSet::hasTemplateSymbolBindings(op)) {
2295 roots.push_back(llvm::cast<SymbolOpInterface>(op));
2296 }
2297 });
2298
2299 Step6_Cleanup::FromKeepSet cleaner(
2300 rootMod, getAnalysis<SymbolDefTree>(), getAnalysis<SymbolUseGraph>()
2301 );
2302 return cleaner.eraseUnreachableFrom(roots);
2303 }
2304
2305 LogicalResult eraseUnreachableFromMainStruct(ModuleOp rootMod, bool emitWarning = true) {
2306 Step6_Cleanup::FromKeepSet cleaner(
2307 rootMod, getAnalysis<SymbolDefTree>(), getAnalysis<SymbolUseGraph>()
2308 );
2309 FailureOr<SymbolLookupResult<StructDefOp>> mainOpt =
2310 getMainInstanceDef(cleaner.tables, rootMod.getOperation());
2311 if (failed(mainOpt)) {
2312 return failure();
2313 }
2314 SymbolLookupResult<StructDefOp> main = mainOpt.value();
2315 if (emitWarning && !main) {
2316 // Emit warning if there is no main specified because all cleanup-candidate definitions not
2317 // reachable from global defs may be removed.
2318 rootMod.emitWarning()
2319 .append(
2320 "using option '", cleanupMode.getArgStr(), '=',
2321 stringifyFlatteningCleanupMode(FlatteningCleanupMode::MainAsRoot), "' with no \"",
2323 "\" attribute on the top-level module may remove all cleanup-candidate definitions!"
2324 )
2325 .report();
2326 }
2327 SmallVector<SymbolOpInterface> roots;
2328 if (main) {
2329 roots.push_back(*main);
2330 }
2331 return cleaner.eraseUnreachableFrom(roots);
2332 }
2333};
2334
2335} // namespace
#define DEBUG_TYPE
Common private implementation for poly dialect passes.
This file defines methods symbol lookup across LLZK operations and included files.
Private parameter substitution helpers shared by polymorphic transformations.
::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:85
::llvm::ArrayRef<::mlir::Attribute > getDimensionSizes() const
::mlir::TypedValue<::llzk::array::ArrayType > getResult()
Definition Ops.h.inc:612
::mlir::DenseI32ArrayAttr getNumDimsPerMapAttr()
Definition Ops.h.inc:625
static constexpr ::llvm::StringLiteral getOperationName()
Definition Ops.h.inc:581
::mlir::OperandRangeRange getMapOperands()
Definition Ops.h.inc:596
::mlir::TypedValue<::mlir::Type > getResult()
Definition Ops.h.inc:1222
::mlir::TypedValue<::mlir::Type > getRvalue()
Definition Ops.h.inc:1417
static constexpr ::llvm::StringLiteral getOperationName()
Definition Ops.h.inc:502
void setType(::mlir::Type attrValue)
Definition Ops.cpp.inc:657
::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:689
static constexpr ::llvm::StringLiteral getOperationName()
Definition Ops.h.inc:1123
StructType getType(::std::optional<::mlir::ArrayAttr > constParams={})
Gets the StructType representing this struct.
::llvm::StringRef getSymName()
Definition Ops.cpp.inc:1910
void setSymName(::llvm::StringRef attrValue)
Definition Ops.cpp.inc:1915
::mlir::SymbolRefAttr getNameRef() const
static StructType get(::mlir::SymbolRefAttr structName)
Definition Types.cpp.inc:81
::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:1004
::llzk::component::StructType getSingleResultTypeOfCompute()
Assuming the callee is FUNC_NAME_COMPUTE, return the single StructType result.
Definition Ops.cpp:1044
::mlir::SymbolRefAttr getCalleeAttr()
Definition Ops.h.inc:456
::llvm::LogicalResult verifySymbolUses(::mlir::SymbolTableCollection &symbolTable)
Definition Ops.cpp:933
bool calleeIsStructCompute()
Return true iff the callee function name is FUNC_NAME_COMPUTE within a StructDefOp.
Definition Ops.cpp:992
::mlir::SymbolRefAttr getCallee()
Definition Ops.cpp.inc:566
void setTemplateParamsAttr(::mlir::ArrayAttr attr)
Definition Ops.h.inc:490
::mlir::Operation::operand_range getArgOperands()
Definition Ops.h.inc:430
::mlir::ArrayAttr getTemplateParamsAttr()
Definition Ops.h.inc:461
::mlir::OperandRangeRange getMapOperands()
Definition Ops.h.inc:434
void setCalleeAttr(::mlir::SymbolRefAttr attr)
Definition Ops.h.inc:486
::mlir::FailureOr<::llzk::SymbolLookupResult<::llzk::function::FuncDefOp > > getCalleeTarget(::mlir::SymbolTableCollection &tables)
Resolve and return the target FuncDefOp for this CallOp.
Definition Ops.cpp:1037
::mlir::DenseI32ArrayAttr getNumDimsPerMapAttr()
Definition Ops.h.inc:466
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:1229
::llvm::ArrayRef<::mlir::Type > getArgumentTypes()
Required by FunctionOpInterface.
Definition Ops.h.inc:1067
::llzk::component::StructType getSingleResultTypeOfCompute()
Assuming the name is FUNC_NAME_COMPUTE, return the single StructType result.
Definition Ops.cpp:517
::llvm::StringRef getSymName()
Definition Ops.cpp.inc:1224
bool isStructCompute()
Return true iff the function is within a StructDefOp and named FUNC_NAME_COMPUTE.
Definition Ops.h.inc:1096
bool isInStruct()
Return true iff the function is within a StructDefOp.
Definition Ops.h.inc:1093
void setFunctionType(::mlir::FunctionType attrValue)
Definition Ops.cpp.inc:1248
void setSymName(::llvm::StringRef attrValue)
Definition Ops.cpp.inc:1244
::mlir::StringAttr getSymNameAttr()
Definition Ops.h.inc:884
::mlir::Operation::operand_range getOperands()
Definition Ops.h.inc:1228
::mlir::Region & getBodyRegion()
Definition Ops.h.inc:1039
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:1130
::mlir::StringAttr getSymNameAttr()
Definition Ops.h.inc:1052
void setSymName(::llvm::StringRef attrValue)
Definition Ops.cpp.inc:1275
::llvm::StringRef getSymName()
Definition Ops.cpp.inc:1270
inline ::llvm::iterator_range<::mlir::Region::op_iterator< OpT > > getConstOps()
Return ops of type OpT within the body region.
Definition Ops.h.inc:1102
::mlir::StringAttr getSymNameAttr()
Definition Ops.h.inc:1357
Shared state for post-instantiation cleanup helpers.
Definition SharedImpl.h:63
CleanupBase(mlir::ModuleOp root, const SymbolDefTree &symDefTree, const SymbolUseGraph &symUseGraph)
Substitute known template bindings in function types and inference candidates.
mlir::Attribute convertAttr(mlir::Attribute attr) const
Substitute a bound attribute or the type contained in a TypeAttr.
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
Common interface for constant parameter-like symbol operations that can be defined within a TemplateO...
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:193
std::optional< Attribute > inferUnifiedParam(const UnificationMap &unifyResult, SymbolRefAttr paramName)
Return the callee-side unification-derived value for a template parameter, if any.
FailureOr< InstantiationLayout > buildInstantiationLayout(TemplateOp parentTemplate, ArrayAttr callParams, const DenseMap< Attribute, Attribute > &paramNameToConcrete)
LogicalResult substituteStructBody(StructDefOp newStruct, StructType typeAtDef, const DenseMap< Attribute, Attribute > &paramNameToConcrete, SmallVector< Diagnostic > &diagnostics)
LogicalResult substituteFunctionBody(FuncDefOp newFunc, const DenseMap< Attribute, Attribute > &paramNameToConcrete, SmallVector< Diagnostic > &delayedDiagnostics)
void evaluateTemplateExprs(TemplateOp templateOp, DenseMap< Attribute, Attribute > &paramNameToConcrete)
Evaluate all TemplateExprOps in templateOp that can be computed from the currently-known concrete par...
void convertCalleesInPlace(Operation *op, const DenseMap< Attribute, Attribute > &paramNameToValue)
void setInstantiationNamePattern(TemplateOp templateOp, ArrayAttr namePattern)
bool isErasableDefinition(mlir::Operation *op)
Return true iff op is a cleanup candidate.
void reportDelayedDiagnostics(Operation *site, SmallVector< Diagnostic > &&diagnostics)
std::unique_ptr<::mlir::Pass > createEmptyTemplateRemovalPass()
::llvm::StringRef stringifyFlatteningCleanupMode(FlatteningCleanupMode val)
OpClass replaceOpWithNewOp(Rewriter &rewriter, mlir::Operation *op, Args &&...args)
Wrapper for RewriterBase::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:262
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:293
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:208
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)
TypeClass getAtIndex(mlir::TypeRange types, size_t index)
Definition TypeHelper.h:297
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.
bool isDynamic(IntegerAttr intAttr)
mlir::SymbolRefAttr asSymbolRefAttr(mlir::StringAttr root, mlir::SymbolRefAttr tail)
Build a SymbolRefAttr that prepends tail with root, i.e., root::tail.
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:132
mlir::ArrayAttr namePattern
The refined literal chunks; fully concrete generated templates clear this state.
Definition SharedImpl.h:139
mlir::SmallVector< mlir::Attribute > remainingNames
Definition SharedImpl.h:133
mlir::ArrayAttr concreteParamKey
Ordered [parameter-name, concrete-value, ...] entries for exact partial-function reuse.
Definition SharedImpl.h:135