LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
LLZKWhileToForPass.cpp
Go to the documentation of this file.
1//===-- LLZKWhileToForPass.cpp ----------------------------------*- 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//===----------------------------------------------------------------------===//
25//===----------------------------------------------------------------------===//
26
34#include "llzk/Util/Field.h"
35
36#include <mlir/Interfaces/SideEffectInterfaces.h>
37
38#include <llvm/Support/Debug.h>
39
40#include <algorithm>
41#include <cassert>
42#include <limits>
43
44namespace llzk {
45#define GEN_PASS_DEF_WHILETOFORPASS
47} // namespace llzk
48
49#define DEBUG_TYPE "while-to-for"
50
51using namespace mlir;
52using namespace scf;
53using namespace llzk::boolean;
54using namespace llzk::felt;
55
56namespace {
57
58struct ForOpInfo {
59 // SSA values holding the loop bounds
60 std::optional<Value> lb, ub, step;
61
62 // Block argument index of the induction variable in the *before* block
63 std::optional<size_t> ivarIndexBefore;
64 // Block argument index of the induction variable in the *after* block
65 std::optional<size_t> ivarIndexAfter;
66
67 bool success() const {
68 return lb.has_value() && ub.has_value() && step.has_value() && ivarIndexBefore.has_value() &&
69 ivarIndexAfter.has_value();
70 }
71};
72
73static inline ForOpInfo parseInfo(WhileOp op) {
74 auto reportFailureReason = [&op](Twine reason) {
75 return op.emitWarning("'llzk-while-to-for' failed to transform loop: ") << reason;
76 };
77
78 ForOpInfo info;
79
80 if (!std::equal(
81 op.getBeforeArguments().begin(), op.getBeforeArguments().end(),
82 op.getConditionOp().getArgs().begin()
83 )) {
84 reportFailureReason("block arguments not passed through from preamble to body");
85 return info;
86 }
87
88 auto condition = op.getConditionOp().getCondition();
89 Value ivarBefore;
90 if (auto cmp = condition.getDefiningOp<CmpOp>();
91 cmp && cmp.getPredicate() == FeltCmpPredicate::LT) {
92 // We found the ivar and the ub
93 ivarBefore = cmp.getLhs();
94 info.ub = cmp.getRhs();
95 } else {
96 reportFailureReason("could not identify an upper bound");
97 return info;
98 }
99
100 auto getBlockArgIndex = [](Value v, ValueRange argList, std::optional<size_t> &index) {
101 for (auto [i, arg] : llvm::enumerate(argList)) {
102 if (arg == v) {
103 index.emplace(i);
104 break;
105 }
106 }
107 };
108
109 // Find which # block arg the ivar is in the before and after blocks
110 getBlockArgIndex(ivarBefore, op.getBeforeArguments(), info.ivarIndexBefore);
111 getBlockArgIndex(ivarBefore, op.getConditionOp().getArgs(), info.ivarIndexAfter);
112
113 if (!info.ivarIndexBefore.has_value() || !info.ivarIndexAfter.has_value()) {
114 reportFailureReason("could not identify an induction variable");
115 return info;
116 }
117
118 // If the yielded final value of the induction variable has any uses, we can't cleanly transform
119 // this to an scf.for (which doesn't explicitly yield its induction var) without doing some extra
120 // computation. Lets just conservatively bail out in that case
121 auto yieldedIVar = op->getResults().drop_front(*info.ivarIndexAfter).front();
122 if (!yieldedIVar.use_empty()) {
123 auto report = reportFailureReason("final ivar value unsafe to drop");
124 for (auto *use : yieldedIVar.getUsers()) {
125 report.attachNote(use->getLoc()) << "used here";
126 }
127 return info;
128 }
129
130 OpBuilder builder(op);
131 // Materialize a felt bound before the loop in the supplied induction variable type.
132 auto createBound = [&builder, &op](const llvm::DynamicAPInt &value, FeltType type) -> Value {
133 auto attr = FeltConstAttr::get(builder.getContext(), value, type);
134 return FeltConstantOp::create(builder, op.getLoc(), attr).getResult();
135 };
136
137 // We need an induction variable anyway, but if the loop has {llzk.loopbounds} we can skip trying
138 // to parse the rest of the bounds and just materialize constants
139 if (op->hasAttr(llzk::LoopBoundsAttr::name)) {
140 auto bounds = op->getAttrOfType<llzk::LoopBoundsAttr>(llzk::LoopBoundsAttr::name);
141 auto ivarType = cast<FeltType>(op.getBeforeArguments()[*info.ivarIndexBefore].getType());
142
143 // Make these constant felts for now; the actual for op builder will later clean it up
144 info.lb = createBound(bounds.getLower(), ivarType);
145 info.ub = createBound(bounds.getUpper(), ivarType);
146 info.step = createBound(bounds.getStep(), ivarType);
147 return info;
148 }
149
150 Value ivarAfter = op.getAfterArguments().drop_front(*info.ivarIndexAfter).front();
151
152 // Now, look for the lb as the corresponding init arg
153 info.lb = *op.getInits().drop_front(*info.ivarIndexBefore).begin();
154
155 // Finally, look for the step
156 auto nextIvar = *op.getYieldedValues().drop_front(*info.ivarIndexBefore).begin();
157 if (auto incOp = nextIvar.getDefiningOp<AddFeltOp>()) {
158 if (incOp.getRhs() == ivarAfter) {
159 info.step = incOp.getLhs();
160 } else if (incOp.getLhs() == ivarAfter) {
161 info.step = incOp.getRhs();
162 }
163 }
164
165 // Constant propagation can replace the final increment of a one-iteration loop
166 // with a constant. Only accept it when the initial condition is true and the
167 // yielded value makes the next condition false; otherwise it may loop forever.
168 if (!info.step.has_value()) {
169 if (auto type = dyn_cast<FeltType>(ivarAfter.getType()); type && type.hasField()) {
170 auto lower = info.lb->getDefiningOp<FeltConstantOp>();
171 auto upper = info.ub->getDefiningOp<FeltConstantOp>();
172 auto next = nextIvar.getDefiningOp<FeltConstantOp>();
173 if (lower && upper && next) {
174 // In valid IR, all 3 constants must have the same type as the induction variable
175 // and we checked above that it has a field so they can all be reduced via the field.
176 assert(lower.getType() == type && "lower bound type must match induction variable");
177 assert(upper.getType() == type && "upper bound type must match induction variable");
178 assert(next.getType() == type && "yielded constant type must match induction variable");
179 const llzk::Field &field = type.getField();
180 auto lo = field.reduce(lower.getValue().getRawValue());
181 auto hi = field.reduce(upper.getValue().getRawValue());
182 auto end = field.reduce(next.getValue().getRawValue());
183 if (lo < hi && end >= hi) {
184 // The for loop casts its bounds and step to index. Since 0 <= lo < hi,
185 // bounding hi also bounds lo and hi - lo.
186 if (hi > std::numeric_limits<int64_t>::max()) {
187 reportFailureReason("upper bound cannot be represented as a nonnegative index");
188 return info;
189 }
190 // A while loop also executes the condition region on its final check.
191 // Dropping that execution is only safe for pure preamble operations.
192 bool purePreamble =
193 llvm::all_of(op.getBefore().front().without_terminator(), [](Operation &preambleOp) {
194 return mlir::isPure(&preambleOp);
195 });
196 if (!purePreamble) {
197 reportFailureReason("condition region contains operations that are not pure");
198 return info;
199 }
200 // Use canonical representatives, including when source literals are negative.
201 info.lb = createBound(lo, type);
202 info.ub = createBound(hi, type);
203 info.step = createBound(hi - lo, type);
204 }
205 }
206 }
207 }
208
209 if (!info.step.has_value()) {
210 reportFailureReason("could not identify step");
211 return info;
212 }
213
214 // Make sure the bounds aren't loop-carried, making this not a legal for loop
215 // We don't actually need to check the LB because its always passed in via an init block arg
216 std::function<bool(Value, InFlightDiagnostic &)> isRuntimeConstant;
217 isRuntimeConstant = [&op, &isRuntimeConstant](Value val, InFlightDiagnostic &reporter) -> bool {
218 // The value can't come from a block argument owned by the while loop
219 if (auto blockArg = dyn_cast<BlockArgument>(val)) {
220 reporter.attachNote(blockArg.getLoc()) << "depends on loop-carried value";
221 return blockArg.getParentBlock()->getParentOp() != op;
222 }
223
224 // The value also can't depend on any value that comes from a block argument owned by the while
225 // loop
226 return !llvm::any_of(val.getDefiningOp()->getOperands(), [&](Value operand) {
227 return !isRuntimeConstant(operand, reporter);
228 });
229 };
230
231 auto ubReport = reportFailureReason("upper bound may not be constant");
232 if (!isRuntimeConstant(*info.ub, ubReport)) {
233 return ForOpInfo {};
234 }
235 ubReport.abandon();
236
237 auto stepReport = reportFailureReason("step may not be constant");
238 if (!isRuntimeConstant(*info.step, stepReport)) {
239 return ForOpInfo {};
240 }
241 stepReport.abandon();
242
243 return info;
244}
245
246static inline FailureOr<scf::ForOp>
247transformWhileToFor(scf::WhileOp op, ForOpInfo info, RewriterBase &rewriter) {
248 llzk::ensure(info.success(), "attempting to convert non-constant while loop");
249
250 rewriter.setInsertionPointAfter(op);
251 IRMapping mapping;
252
253 auto copyIfNeeded = [op, &rewriter, &mapping](Value val) -> Value {
254 if (auto *definingOp = val.getDefiningOp();
255 definingOp && definingOp->getParentOfType<scf::WhileOp>() == op) {
256 return rewriter.clone(*definingOp, mapping)->getResult(0);
257 }
258 return val;
259 };
260
261 // scf.for bounds have to be `index`, so we might have to cast them here felt -> index before
262 // building the op, and cast them back index -> felt inside the body
263 auto toIndex = [&rewriter](Value val) -> Value {
264 if (!isa<FeltType>(val.getType())) {
265 return val;
266 }
267 return llzk::cast::FeltToIndexOp::create(rewriter, val.getLoc(), val).getResult();
268 };
269
270 // Emit a prelude setting up the loop bounds
271 auto lb = copyIfNeeded(*info.lb);
272 auto ub = copyIfNeeded(*info.ub);
273 auto step = copyIfNeeded(*info.step);
274
275 // Store the original type of the scf.while's induction var so we can cast back if necessary
277 lb.getType() == ub.getType() && lb.getType() == step.getType(),
278 "cannot have differing types for loop bounds"
279 );
280 Type ivarType = lb.getType();
281 if (isa<FeltType>(ivarType)) {
282 lb = toIndex(lb);
283 ub = toIndex(ub);
284 step = toIndex(step);
285 }
286
287 SmallVector<Value> inits;
288 for (auto [i, init] : llvm::enumerate(op.getInits())) {
289 if (i == info.ivarIndexBefore) {
290 continue;
291 }
292 inits.push_back(init);
293 }
294
295 // Build the skeleton of the for loop
297 op, scf::ForOp::create(rewriter, op->getLoc(), lb, ub, step, inits)
298 );
299 rewriter.setInsertionPointToStart(forOp.getBody());
300
301 auto inductionVar = forOp.getInductionVar();
302 if (isa<FeltType>(ivarType)) {
303 // If the induction var was a felt, we need to cast it back to felt in the scf.for body
304 // Note that this means the body of the scf.for might cast it back to index again anyway, but
305 // --canonicalize should fix that
306 inductionVar = llzk::cast::IntToFeltOp::create(rewriter, forOp.getLoc(), ivarType, inductionVar)
307 .getResult();
308 }
309
310 // Start by mapping the `before` block to the loop body
311 // Each block arg of the before block should get mapped to the corresponding iter_arg, with the
312 // exception of the induction var which should get mapped to the induction var
313 auto *whilePreamble = op.getBeforeBody();
314 for (size_t i = 0; i < whilePreamble->getNumArguments(); i++) {
315 if (i == info.ivarIndexBefore) {
316 mapping.map(whilePreamble->getArgument(i), inductionVar);
317 continue;
318 }
319 mapping.map(
320 whilePreamble->getArgument(i), forOp.getRegionIterArg(i > info.ivarIndexBefore ? i - 1 : i)
321 );
322 }
323
324 // Emit the preamble into the for loop body
325 for (auto &preambleOp : *whilePreamble) {
326 // Don't emit the scf.condition; rather, use it to update the mapping in preparation for the
327 // loop body
328 if (auto condOp = dyn_cast<scf::ConditionOp>(&preambleOp)) {
329 for (auto [value, blockArg] : llvm::zip(condOp.getArgs(), op.getAfterArguments())) {
330 // TODO: maybe this isn't transitive?
331 mapping.map(blockArg, mapping.lookupOrDefault(value));
332 }
333 continue;
334 }
335 rewriter.clone(preambleOp, mapping);
336 }
337
338 auto *whileBody = op.getAfterBody();
339
340 for (auto &bodyOp : *whileBody) {
341 // scf.yield is special here because we don't want to yield the induction var to the next
342 // iteration...
343 if (auto yieldOp = dyn_cast<scf::YieldOp>(&bodyOp)) {
344 SmallVector<Value> valuesToYield;
345 for (auto [i, val] : llvm::enumerate(yieldOp.getResults())) {
346 if (i == info.ivarIndexAfter) {
347 continue;
348 }
349 // ...but the other yielded values should point to the correct iter_arg
350 valuesToYield.push_back(mapping.lookupOrDefault(val));
351 }
352 if (!valuesToYield.empty()) {
353 scf::YieldOp::create(rewriter, yieldOp.getLoc(), valuesToYield);
354 }
355 continue;
356 }
357 rewriter.clone(bodyOp, mapping);
358 }
359
360 // scf.for doesn't explicitly yield its induction var from the final iteration, so we need to
361 // reconstruct it
362 SmallVector<Value> replacedValues;
363 for (auto [i, result] : llvm::enumerate(op.getResults())) {
364 if (i == info.ivarIndexBefore) {
365 // Note that the final value of the induction variable might not actually be the upper bound
366 // (e.g. if `step` doesn't divide `(ub - lb)`), but we've already guaranteed earlier that this
367 // value isn't being used so it doesn't matter what gets yielded here (the canonicalizer can
368 // clean it up). But we still have to yield something to preserve the shape of the op.
369 replacedValues.push_back(*info.ub);
370 continue;
371 }
372
373 replacedValues.push_back(forOp.getResult(i > info.ivarIndexBefore ? i - 1 : i));
374 }
375
376 rewriter.replaceOp(op, replacedValues);
377 return forOp;
378}
379
380class PassImpl : public llzk::impl::WhileToForPassBase<PassImpl> {
381 using Base = WhileToForPassBase<PassImpl>;
382 using Base::Base;
383
384 void getDependentDialects(DialectRegistry &registry) const override {
385 registry.insert<llzk::cast::CastDialect>();
386 }
387 void runOnOperation() override {
388 Operation *op = getOperation();
389 IRRewriter rewriter {op->getContext()};
390 auto result = op->walk([&rewriter](scf::WhileOp loop) -> WalkResult {
391 ForOpInfo info = parseInfo(loop);
392 if (!info.success()) {
393 // Ignore loops we can't prove have constant bounds
394 return WalkResult::advance();
395 }
396 return LogicalResult(transformWhileToFor(loop, info, rewriter));
397 });
398 if (result.wasInterrupted()) {
399 signalPassFailure();
400 }
401 }
402};
403
404} // namespace
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
Definition LICENSE.txt:9
Information about the prime finite field used for the interval analysis.
Definition Field.h:36
llvm::DynamicAPInt reduce(const llvm::DynamicAPInt &i) const
Returns the canonical representative of i mod p in [0, p).
static const Field & getField(llvm::StringRef fieldName, EmitErrorFn errFn)
Get a Field from a given field name string.
LLZK type conversion operations.
Definition Dialect.h.inc:13
static FeltToIndexOp create(::mlir::OpBuilder &builder, ::mlir::Location location, ::mlir::Type result, ::llzk::cast::OverflowSemanticsAttr overflow, ::mlir::Value value)
Definition Ops.cpp.inc:203
::mlir::TypedValue<::mlir::IndexType > getResult()
Definition Ops.h.inc:231
::mlir::TypedValue<::llzk::felt::FeltType > getResult()
Definition Ops.h.inc:509
static IntToFeltOp create(::mlir::OpBuilder &builder, ::mlir::Location location, ::mlir::Type result, ::llzk::cast::OverflowSemanticsAttr overflow, ::mlir::Value value)
Definition Ops.cpp.inc:703
::mlir::TypedValue<::mlir::Type > getResult()
Definition Ops.h.inc:1017
static FeltConstantOp create(::mlir::OpBuilder &builder, ::mlir::Location location, ::mlir::Type result, ::llzk::felt::FeltConstAttr value)
Definition Ops.cpp.inc:1274
::llzk::felt::FeltConstAttr getValue()
Definition Ops.cpp.inc:1264
void ensure(bool condition, const llvm::Twine &errMsg)
ExpressionValue cmp(const llvm::SMTSolverRef &solver, CmpOp op, const ExpressionValue &lhs, const ExpressionValue &rhs)
OpTy preserveDiscardableAttrs(mlir::Operation *src, OpTy dst)
Copy discardable attributes from src to dst.