LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
SpecializedMemoryPasses.h
Go to the documentation of this file.
1//===-- SpecializedMemoryPasses.h - Targeted SROA / mem2reg -----*- C++ -*-===//
2//
3// Part of the LLZK Project, under the Apache License v2.0.
4// See LICENSE.txt for license information.
5// Copyright 2026 Project LLZK
6// SPDX-License-Identifier: Apache-2.0
7//
8//===----------------------------------------------------------------------===//
16//===----------------------------------------------------------------------===//
17
18#pragma once
19
22#include "llzk/Util/Walk.h"
23
24#include <mlir/Analysis/DataLayoutAnalysis.h>
25#include <mlir/Dialect/SCF/IR/SCF.h>
26#include <mlir/Dialect/UB/IR/UBOps.h>
27#include <mlir/IR/Dominance.h>
28#include <mlir/Interfaces/MemorySlotInterfaces.h>
29#include <mlir/Pass/Pass.h>
30#include <mlir/Pass/PassManager.h>
31#include <mlir/Transforms/Mem2Reg.h>
32#include <mlir/Transforms/Passes.h>
33#include <mlir/Transforms/SROA.h>
34
35#include <llvm/ADT/StringRef.h>
36
37#include <memory>
38
39namespace llzk {
40
44template <typename AllocOpTy>
45struct SpecializedSROA : mlir::PassWrapper<SpecializedSROA<AllocOpTy>, mlir::OperationPass<>> {
46
47 mlir::StringRef getArgument() const override { return "llzk-specialized-sroa"; }
48
49 mlir::StringRef getDescription() const override {
50 return "Scalar replacement of aggregates for a specific allocator op type";
51 }
52
53 void runOnOperation() override {
54 mlir::Operation *scopeOp = this->getOperation();
55
56 auto &dataLayoutAnalysis = this->template getAnalysis<mlir::DataLayoutAnalysis>();
57 const mlir::DataLayout &dataLayout = dataLayoutAnalysis.getAtOrAbove(scopeOp);
58
59 bool changed = false;
60
61 for (mlir::Region &region : scopeOp->getRegions()) {
62 if (region.getBlocks().empty()) {
63 continue;
64 }
65
66 mlir::OpBuilder builder(&region.front(), region.front().begin());
67
68 auto allocators = walkCollectMapped<AllocOpTy>(region, [](auto allocator) {
69 return mlir::DestructurableAllocationOpInterface(allocator);
70 });
71
72 if (mlir::succeeded(mlir::tryToDestructureMemorySlots(allocators, builder, dataLayout))) {
73 changed = true;
74 }
75 }
76
77 if (!changed) {
78 this->markAllAnalysesPreserved();
79 }
80 }
81};
82
83// Pass factory for `SpecializedSROA`.
84template <typename AllocOpTy>
85std::unique_ptr<SpecializedSROA<AllocOpTy>> createSpecializedSROAPass() {
86 return std::make_unique<SpecializedSROA<AllocOpTy>>();
87}
88
92template <typename AllocOpTy>
94 : mlir::PassWrapper<SpecializedMem2Reg<AllocOpTy>, mlir::OperationPass<>> {
95
96 mlir::StringRef getArgument() const override { return "llzk-specialized-mem2reg"; }
97
98 mlir::StringRef getDescription() const override {
99 return "Promotes memory slots of a specific allocator op type into values";
100 }
101
102 void runOnOperation() override {
103 mlir::Operation *scopeOp = this->getOperation();
104
105 auto &dataLayoutAnalysis = this->template getAnalysis<mlir::DataLayoutAnalysis>();
106 const mlir::DataLayout &dataLayout = dataLayoutAnalysis.getAtOrAbove(scopeOp);
107 auto &dominance = this->template getAnalysis<mlir::DominanceInfo>();
108
109 bool changed = false;
110
111 for (mlir::Region &region : scopeOp->getRegions()) {
112 if (region.getBlocks().empty()) {
113 continue;
114 }
115
116 mlir::OpBuilder builder(&region.front(), region.front().begin());
117
118 auto allocators = walkCollectMapped<AllocOpTy>(region, [](auto allocator) {
119 return mlir::PromotableAllocationOpInterface(allocator);
120 });
121
122 auto promoteRes = mlir::tryToPromoteMemorySlots(allocators, builder, dataLayout, dominance);
123 if (mlir::succeeded(promoteRes)) {
124 changed = true;
125 }
126 }
127
128 if (!changed) {
129 this->markAllAnalysesPreserved();
130 }
131 }
132};
133
134// Pass factory for `SpecializedMem2Reg`.
135template <typename AllocOpTy>
136std::unique_ptr<SpecializedMem2Reg<AllocOpTy>> createSpecializedMem2RegPass() {
137 return std::make_unique<SpecializedMem2Reg<AllocOpTy>>();
138}
139
140namespace detail {
141
146 : public mlir::PassWrapper<RemoveDeadValuesWorkaroundPass, mlir::OperationPass<>> {
147
148 static inline std::unique_ptr<mlir::Pass> createRemoveDeadValuesPass() {
149 return mlir::createRemoveDeadValuesPass(
150 mlir::RemoveDeadValuesPassOptions {.canonicalize = true}
151 );
152 }
153
154public:
155 MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(RemoveDeadValuesWorkaroundPass)
156
157 llvm::StringRef getArgument() const override { return "remove-dead-values"; }
158 llvm::StringRef getDescription() const override { return "Remove dead values"; }
159
160 void getDependentDialects(mlir::DialectRegistry &registry) const override {
161 createRemoveDeadValuesPass()->getDependentDialects(registry);
162 registry.insert<LLZKDialect>();
163 }
164
165 void runOnOperation() final {
166 mlir::Operation *scopeOp = this->getOperation();
167
168 mlir::OpPassManager pm(scopeOp->getName().getStringRef());
169 pm.addPass(createRemoveDeadValuesPass());
170 if (mlir::failed(runPipeline(pm, scopeOp))) {
171 signalPassFailure();
172 }
173
174 // Upstream RDV only tracks poison values created during its current invocation. This wrapper
175 // may run repeatedly, so values created by an earlier invocation can become unused later.
176 // Additionally, replace any remaining uses of `ub.poison` with LLZK native `llzk.nondet` op.
177 for (mlir::ub::PoisonOp poisonOp : walkCollect<mlir::ub::PoisonOp>(*scopeOp)) {
178 auto poisonResult = poisonOp.getResult();
179 if (!poisonResult.use_empty()) {
180 mlir::OpBuilder builder(poisonOp);
181 auto nondetOp = NonDetOp::create(builder, poisonOp.getLoc(), poisonOp.getType());
182 poisonResult.replaceAllUsesWith(nondetOp.getResult());
183 }
184 poisonOp.erase();
185 }
186
187 // Finally, those `ub.poison` removals can leave behind trivial `else` blocks that would have
188 // otherwise been removed by RDV so go ahead and remove them now.
189 scopeOp->walk([](mlir::scf::IfOp ifOp) {
190 if (ifOp.getResults().empty()) {
191 mlir::Region &elseRegion = ifOp.getElseRegion();
192 if (!llvm::hasSingleElement(elseRegion)) {
193 return;
194 }
195 mlir::Block &elseBlock = elseRegion.front();
196 if (!llvm::hasSingleElement(elseBlock)) {
197 return;
198 }
199 if (!llvm::isa<mlir::scf::YieldOp>(elseBlock.front())) {
200 return;
201 }
202 elseRegion.dropAllReferences();
203 elseBlock.clear();
204 elseRegion.getBlocks().clear();
205 }
206 });
207 }
208};
209
210} // namespace detail
211
212inline std::unique_ptr<mlir::Pass> createRemoveDeadValuesWorkaroundPass() {
213 return std::make_unique<detail::RemoveDeadValuesWorkaroundPass>();
214}
215
216} // namespace llzk
Common LLZK attributes.
Definition Dialect.h.inc:13
static NonDetOp create(::mlir::OpBuilder &builder, ::mlir::Location location, ::mlir::Type res)
Definition Ops.cpp.inc:94
A workaround wrapper around MLIR's remove-dead-values pass that normalizes empty scf....
void getDependentDialects(mlir::DialectRegistry &registry) const override
std::unique_ptr< mlir::Pass > createRemoveDeadValuesWorkaroundPass()
std::unique_ptr< SpecializedMem2Reg< AllocOpTy > > createSpecializedMem2RegPass()
std::unique_ptr< SpecializedSROA< AllocOpTy > > createSpecializedSROAPass()
A variant of the MLIR mem2reg pass that only promotes memory slots belonging to allocators of type Al...
mlir::StringRef getArgument() const override
mlir::StringRef getDescription() const override
A variant of the MLIR sroa pass that only destructures memory slots belonging to allocators of type A...
mlir::StringRef getDescription() const override
mlir::StringRef getArgument() const override