LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
LLZKPolyLoweringPass.cpp
Go to the documentation of this file.
1//===-- LLZKPolyLoweringPass.cpp --------------------------------*- 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// Copyright 2026 Project LLZK
7// SPDX-License-Identifier: Apache-2.0
8//
9//===----------------------------------------------------------------------===//
14//===----------------------------------------------------------------------===//
15
22
23#include <mlir/IR/BuiltinOps.h>
24#include <mlir/IR/Dominance.h>
25
26#include <llvm/ADT/DenseMap.h>
27#include <llvm/ADT/DenseMapInfo.h>
28#include <llvm/ADT/DenseSet.h>
29#include <llvm/ADT/STLExtras.h>
30#include <llvm/ADT/ScopeExit.h>
31#include <llvm/ADT/SmallVector.h>
32#include <llvm/ADT/StringMap.h>
33#include <llvm/Support/Debug.h>
34#include <llvm/Support/raw_ostream.h>
35
36#include <deque>
37#include <memory>
38#include <optional>
39#include <stdexcept>
40
41// Include the generated base pass class definitions.
42namespace llzk {
43#define GEN_PASS_DEF_POLYLOWERINGPASS
45} // namespace llzk
46
47using namespace mlir;
48using namespace llzk;
49using namespace llzk::felt;
50using namespace llzk::function;
51using namespace llzk::component;
52using namespace llzk::constrain;
53using namespace llzk::array;
54
55#define DEBUG_TYPE "llzk-poly-lowering-pass"
56#define AUXILIARY_MEMBER_PREFIX "__llzk_poly_lowering_pass_aux_member_"
57
58namespace {
59
60struct AuxAssignment {
61 std::string auxMemberName;
62 Value computedValue;
63 Value auxValue;
64};
65
67struct MutableContainmentElement {
68 ArrayAttr index;
69 OpOperand *operand;
70};
71
72enum class AuxAssignmentVisitState : uint8_t {
73 Unvisited,
74 Visiting,
75 Done,
76};
77
78class DegreeComputationError : public std::runtime_error {
79public:
80 DegreeComputationError(Location errorLoc, const std::string &message)
81 : std::runtime_error(message), loc(errorLoc) {}
82
83 Location getLoc() const { return loc; }
84
85private:
86 Location loc;
87};
88
89class PassImpl : public llzk::impl::PolyLoweringPassBase<PassImpl> {
90 using Base = PolyLoweringPassBase<PassImpl>;
91 using Base::Base;
92
93 unsigned auxCounter = 0;
94
95 static void collectStructDefs(ModuleOp modOp, SmallVectorImpl<StructDefOp> &structDefs) {
96 modOp.walk([&structDefs](StructDefOp structDef) {
97 structDefs.push_back(structDef);
98 return WalkResult::skip();
99 });
100 }
101
103 static void addAuxDependency(
104 unsigned dep, unsigned owner, DenseSet<unsigned> &seenDeps, SmallVectorImpl<unsigned> &deps
105 ) {
106 if (dep == owner) {
107 return;
108 }
109 if (seenDeps.insert(dep).second) {
110 deps.push_back(dep);
111 }
112 }
113
115 static void collectAuxDependencies(
116 Value val, unsigned owner, const DenseMap<Value, unsigned> &auxValueToIndex,
117 const llvm::StringMap<unsigned> &auxNameToIndex, DenseSet<Value> &visitedValues,
118 DenseSet<unsigned> &seenDeps, SmallVectorImpl<unsigned> &deps
119 ) {
120 // Aux dependencies can appear as generated aux SSA values or reads of generated
121 // aux members, so track both forms before ordering writes.
122 if (!val || !visitedValues.insert(val).second) {
123 return;
124 }
125
126 if (auto it = auxValueToIndex.find(val); it != auxValueToIndex.end()) {
127 addAuxDependency(it->second, owner, seenDeps, deps);
128 }
129
130 if (Operation *defOp = val.getDefiningOp()) {
131 if (auto readOp = llvm::dyn_cast<MemberReadOp>(defOp)) {
132 auto it = auxNameToIndex.find(readOp.getMemberName());
133 if (it != auxNameToIndex.end()) {
134 addAuxDependency(it->second, owner, seenDeps, deps);
135 }
136 }
137
138 for (Value operand : defOp->getOperands()) {
139 collectAuxDependencies(
140 operand, owner, auxValueToIndex, auxNameToIndex, visitedValues, seenDeps, deps
141 );
142 }
143 }
144 }
145
147 static LogicalResult visitAuxAssignment(
148 unsigned idx, ArrayRef<SmallVector<unsigned>> deps,
149 SmallVectorImpl<AuxAssignmentVisitState> &visitState, SmallVectorImpl<unsigned> &ordered,
150 ArrayRef<AuxAssignment> auxAssignments
151 ) {
152 if (visitState[idx] == AuxAssignmentVisitState::Done) {
153 return success();
154 }
155 if (visitState[idx] == AuxAssignmentVisitState::Visiting) {
156 return emitError(auxAssignments[idx].computedValue.getLoc())
157 << "poly lowering generated cyclic auxiliary dependency involving @"
158 << auxAssignments[idx].auxMemberName;
159 }
160
161 visitState[idx] = AuxAssignmentVisitState::Visiting;
162 // Emit prerequisite aux writes before the aux writes that read them.
163 for (unsigned dep : deps[idx]) {
164 if (failed(visitAuxAssignment(dep, deps, visitState, ordered, auxAssignments))) {
165 return failure();
166 }
167 }
168 visitState[idx] = AuxAssignmentVisitState::Done;
169 ordered.push_back(idx);
170 return success();
171 }
172
174 static LogicalResult
175 orderAuxAssignments(ArrayRef<AuxAssignment> auxAssignments, SmallVectorImpl<unsigned> &ordered) {
176 DenseMap<Value, unsigned> auxValueToIndex;
177 llvm::StringMap<unsigned> auxNameToIndex;
178 auxValueToIndex.reserve(auxAssignments.size());
179 for (auto [idx, assign] : llvm::enumerate(auxAssignments)) {
180 if (assign.auxValue) {
181 auxValueToIndex[assign.auxValue] = idx;
182 }
183 auxNameToIndex[assign.auxMemberName] = idx;
184 }
185
186 SmallVector<SmallVector<unsigned>> deps(auxAssignments.size());
187 for (auto [idx, assign] : llvm::enumerate(auxAssignments)) {
188 DenseSet<Value> visitedValues;
189 DenseSet<unsigned> seenDeps;
190 collectAuxDependencies(
191 assign.computedValue, idx, auxValueToIndex, auxNameToIndex, visitedValues, seenDeps,
192 deps[idx]
193 );
194 }
195
196 SmallVector<AuxAssignmentVisitState> visitState(
197 auxAssignments.size(), AuxAssignmentVisitState::Unvisited
198 );
199 for (unsigned idx = 0, e = auxAssignments.size(); idx < e; ++idx) {
200 if (failed(visitAuxAssignment(idx, deps, visitState, ordered, auxAssignments))) {
201 return failure();
202 }
203 }
204 return success();
205 }
206
207 // Recursively compute degree of FeltOps SSA values
208 unsigned getDegree(Value val, DenseMap<Value, unsigned> &memo) {
209 if (auto it = memo.find(val); it != memo.end()) {
210 return it->second;
211 }
212 // Handle function parameters (BlockArguments)
213 if (llvm::isa<BlockArgument>(val)) {
214 return memo[val] = 1;
215 }
216 if (Operation *defOp = val.getDefiningOp()) {
217 if (llvm::isa<FeltConstantOp>(defOp)) {
218 return memo[val] = 0;
219 }
220 if (llvm::isa<NonDetOp, MemberReadOp>(defOp)) {
221 return memo[val] = 1;
222 }
223 if (auto add = llvm::dyn_cast<AddFeltOp>(defOp)) {
224 return memo[val] = std::max(getDegree(add.getLhs(), memo), getDegree(add.getRhs(), memo));
225 }
226 if (auto sub = llvm::dyn_cast<SubFeltOp>(defOp)) {
227 return memo[val] = std::max(getDegree(sub.getLhs(), memo), getDegree(sub.getRhs(), memo));
228 }
229 if (auto mul = llvm::dyn_cast<MulFeltOp>(defOp)) {
230 return memo[val] = getDegree(mul.getLhs(), memo) + getDegree(mul.getRhs(), memo);
231 }
232 if (auto div = llvm::dyn_cast<DivFeltOp>(defOp)) {
233 return memo[val] = getDegree(div.getLhs(), memo) + getDegree(div.getRhs(), memo);
234 }
235 if (auto neg = llvm::dyn_cast<NegFeltOp>(defOp)) {
236 return memo[val] = getDegree(neg.getOperand(), memo);
237 }
238 if (auto call = llvm::dyn_cast<CallOp>(defOp)) {
239 std::string message;
240 llvm::raw_string_ostream(message)
241 << "Encountered '" << CallOp::getOperationName()
242 << "' in degree computation. Try running '-llzk-inline-free-functions' first.";
243 throw DegreeComputationError(val.getLoc(), message);
244 }
245 }
246
247 std::string message;
248 llvm::raw_string_ostream(message) << "Unhandled value in degree computation: " << val;
249 throw DegreeComputationError(val.getLoc(), message);
250 }
251
252 Value lowerExpression(
253 Value val, StructDefOp structDef, FuncDefOp constrainFunc, Operation *useOp,
254 DominanceInfo &dominanceInfo, DenseMap<Value, unsigned> &degreeMemo,
255 DenseMap<Value, Value> &rewrites, SmallVector<AuxAssignment> &auxAssignments
256 ) {
257 auto rewriteIt = rewrites.find(val);
258 if (rewriteIt != rewrites.end() && dominanceInfo.properlyDominates(rewriteIt->second, useOp)) {
259 return rewriteIt->second;
260 }
261
262 auto cacheIdentityRewriteIfAbsent = [&rewrites, &val]() {
263 // Keep an existing cached aux replacement for later uses it may dominate.
264 if (!rewrites.contains(val)) {
265 rewrites[val] = val;
266 }
267 };
268
269 unsigned degree = getDegree(val, degreeMemo);
270 if (degree <= maxDegree) {
271 // A cached replacement that does not dominate this use may still be the
272 // right replacement for later uses. Return the original value for this
273 // use without clobbering that scoped rewrite.
274 cacheIdentityRewriteIfAbsent();
275 return val;
276 }
277
278 // Degree-neutral roots can still contain over-degree operands.
279 auto lowerBinaryRoot = [&](auto op) -> Value {
280 Value lhs = lowerExpression(
281 op.getLhs(), structDef, constrainFunc, op.getOperation(), dominanceInfo, degreeMemo,
282 rewrites, auxAssignments
283 );
284 Value rhs = lowerExpression(
285 op.getRhs(), structDef, constrainFunc, op.getOperation(), dominanceInfo, degreeMemo,
286 rewrites, auxAssignments
287 );
288
289 if (lhs != op.getLhs()) {
290 op.getLhsMutable().set(lhs);
291 }
292 if (rhs != op.getRhs()) {
293 op.getRhsMutable().set(rhs);
294 }
295 degreeMemo[val] = std::max(getDegree(lhs, degreeMemo), getDegree(rhs, degreeMemo));
296 cacheIdentityRewriteIfAbsent();
297 return val;
298 };
299
300 Operation *defOp = val.getDefiningOp();
301 if (auto addOp = llvm::dyn_cast_if_present<AddFeltOp>(defOp)) {
302 return lowerBinaryRoot(addOp);
303 } else if (auto subOp = llvm::dyn_cast_if_present<SubFeltOp>(defOp)) {
304 return lowerBinaryRoot(subOp);
305 } else if (auto negOp = llvm::dyn_cast_if_present<NegFeltOp>(defOp)) {
306 Value operand = lowerExpression(
307 negOp.getOperand(), structDef, constrainFunc, negOp.getOperation(), dominanceInfo,
308 degreeMemo, rewrites, auxAssignments
309 );
310
311 if (operand != negOp.getOperand()) {
312 negOp.getOperandMutable().set(operand);
313 }
314 degreeMemo[val] = getDegree(operand, degreeMemo);
315 cacheIdentityRewriteIfAbsent();
316 return val;
317 } else if (auto mulOp = llvm::dyn_cast_if_present<MulFeltOp>(defOp)) {
318 // Recursively lower operands first
319 Value lhs = lowerExpression(
320 mulOp.getLhs(), structDef, constrainFunc, mulOp.getOperation(), dominanceInfo, degreeMemo,
321 rewrites, auxAssignments
322 );
323 Value rhs = lowerExpression(
324 mulOp.getRhs(), structDef, constrainFunc, mulOp.getOperation(), dominanceInfo, degreeMemo,
325 rewrites, auxAssignments
326 );
327
328 unsigned lhsDeg = getDegree(lhs, degreeMemo);
329 unsigned rhsDeg = getDegree(rhs, degreeMemo);
330
331 OpBuilder builder(mulOp.getOperation()->getBlock(), ++Block::iterator(mulOp));
332 Value selfVal = constrainFunc.getSelfValueFromConstrain();
333 bool eraseMul = lhsDeg + rhsDeg > maxDegree;
334 // Optimization: If lhs == rhs, factor it only once
335 if (lhs == rhs && eraseMul) {
336 std::string auxName = AUXILIARY_MEMBER_PREFIX + std::to_string(this->auxCounter++);
337 MemberDefOp auxMember = addAuxMember(structDef, auxName, lhs.getType());
338
339 auto auxVal = builder.create<MemberReadOp>(
340 lhs.getLoc(), lhs.getType(), selfVal, auxMember.getNameAttr()
341 );
342 auxAssignments.push_back({auxName, lhs, auxVal});
343 Location loc = builder.getFusedLoc({auxVal.getLoc(), lhs.getLoc()});
344 auto eqOp = builder.create<EmitEqualityOp>(loc, auxVal, lhs);
345
346 // Memoize auxVal as degree 1
347 degreeMemo[auxVal] = 1;
348 rewrites[lhs] = auxVal;
349 rewrites[rhs] = auxVal;
350 // Now selectively replace subsequent uses of lhs with auxVal
351 replaceSubsequentUsesWith(lhs, auxVal, eqOp);
352
353 // Update lhs and rhs to use auxVal
354 lhs = auxVal;
355 rhs = auxVal;
356
357 lhsDeg = rhsDeg = 1;
358 }
359 // While their product exceeds maxDegree, factor out one side
360 while (lhsDeg + rhsDeg > maxDegree) {
361 Value &toFactor = (lhsDeg >= rhsDeg) ? lhs : rhs;
362
363 // Create auxiliary member for toFactor
364 std::string auxName = AUXILIARY_MEMBER_PREFIX + std::to_string(this->auxCounter++);
365 MemberDefOp auxMember = addAuxMember(structDef, auxName, toFactor.getType());
366
367 // Read back as MemberReadOp (new SSA value)
368 auto auxVal = builder.create<MemberReadOp>(
369 toFactor.getLoc(), toFactor.getType(), selfVal, auxMember.getNameAttr()
370 );
371
372 // Emit constraint: auxVal == toFactor
373 Location loc = builder.getFusedLoc({auxVal.getLoc(), toFactor.getLoc()});
374 auto eqOp = builder.create<EmitEqualityOp>(loc, auxVal, toFactor);
375 auxAssignments.push_back({auxName, toFactor, auxVal});
376 // Update memoization
377 rewrites[toFactor] = auxVal;
378 degreeMemo[auxVal] = 1; // stays same
379 // replace the term with auxVal.
380 replaceSubsequentUsesWith(toFactor, auxVal, eqOp);
381
382 // Remap toFactor to auxVal for next iterations
383 toFactor = auxVal;
384
385 // Recompute degrees
386 lhsDeg = getDegree(lhs, degreeMemo);
387 rhsDeg = getDegree(rhs, degreeMemo);
388 }
389
390 // Now lhs * rhs fits within degree bound
391 auto mulVal = builder.create<MulFeltOp>(lhs.getLoc(), lhs.getType(), lhs, rhs);
392 if (eraseMul) {
393 mulOp->replaceAllUsesWith(mulVal);
394 mulOp->erase();
395 }
396
397 // Result of this multiply has degree lhsDeg + rhsDeg
398 degreeMemo[mulVal] = lhsDeg + rhsDeg;
399 rewrites[val] = mulVal;
400
401 return mulVal;
402 }
403
404 // Unsupported roots are left unchanged.
405 cacheIdentityRewriteIfAbsent();
406 return val;
407 }
408
409 Value materializeCallArgument(
410 Value val, StructDefOp structDef, FuncDefOp constrainFunc, CallOp callOp,
411 DominanceInfo &dominanceInfo, DenseMap<Value, unsigned> &degreeMemo,
412 DenseMap<Value, Value> &rewrites, SmallVector<AuxAssignment> &auxAssignments
413 ) {
414 Value loweredVal = lowerExpression(
415 val, structDef, constrainFunc, callOp.getOperation(), dominanceInfo, degreeMemo, rewrites,
416 auxAssignments
417 );
418 DenseMap<Value, unsigned> checkMemo;
419 if (getDegree(loweredVal, checkMemo) <= 1) {
420 return loweredVal;
421 }
423 // Callees only receive SSA values, not the caller expression tree, so nonlinear
424 // call arguments must be represented by an auxiliary member read.
425 std::string auxName = AUXILIARY_MEMBER_PREFIX + std::to_string(this->auxCounter++);
426 MemberDefOp auxMember = addAuxMember(structDef, auxName, loweredVal.getType());
428 OpBuilder builder(callOp);
429 Value selfVal = constrainFunc.getSelfValueFromConstrain();
430 auto auxVal = builder.create<MemberReadOp>(
431 loweredVal.getLoc(), loweredVal.getType(), selfVal, auxMember.getNameAttr()
432 );
434 Location loc = builder.getFusedLoc({auxVal.getLoc(), loweredVal.getLoc()});
435 builder.create<EmitEqualityOp>(loc, auxVal, loweredVal);
436 auxAssignments.push_back({auxName, loweredVal, auxVal});
437
438 degreeMemo[auxVal] = 1;
439 rewrites[loweredVal] = auxVal;
440 rewrites[val] = auxVal;
441 return auxVal;
442 }
443
444 LogicalResult checkEqualityDegrees(FuncDefOp constrainFunc) {
445 auto res = constrainFunc.walk([this](EmitEqualityOp eqOp) -> WalkResult {
446 Value lhs = eqOp.getLhs();
447 Value rhs = eqOp.getRhs();
448 if (llvm::isa<FeltType>(lhs.getType()) && llvm::isa<FeltType>(rhs.getType())) {
449 DenseMap<Value, unsigned> checkMemo;
450 unsigned lhsDegree = getDegree(lhs, checkMemo);
451 unsigned rhsDegree = getDegree(rhs, checkMemo);
452
453 if (lhsDegree > maxDegree || rhsDegree > maxDegree) {
454 return eqOp.emitOpError().append(
455 "poly lowering postcondition failed: equality operand degree exceeds max-degree ",
456 maxDegree.getValue(), " (lhs degree ", lhsDegree, ", rhs degree ", rhsDegree, ')'
457 );
458 }
459 }
460 return WalkResult::advance();
461 });
462 return failure(res.wasInterrupted());
463 }
464
465 LogicalResult checkStructConstrainCallArguments(FuncDefOp constrainFunc) {
466 auto res = constrainFunc.walk([this](CallOp callOp) -> WalkResult {
468 for (Value arg : callOp.getArgOperands()) {
469 if (!llvm::isa<FeltType>(arg.getType())) {
470 continue;
472 DenseMap<Value, unsigned> checkMemo;
473 unsigned argDegree = getDegree(arg, checkMemo);
474 if (argDegree > 1) {
475 return callOp.emitOpError()
476 << "poly lowering postcondition failed: "
477 "struct constrain call argument degree exceeds 1 (argument degree "
478 << argDegree << ')';
479 }
480 }
481 }
482 return WalkResult::advance();
483 });
484 return failure(res.wasInterrupted());
485 }
486
487
488 static bool isFeltArray(Type type) {
489 if (auto arrayType = llvm::dyn_cast<ArrayType>(type)) {
490 return llvm::isa<FeltType>(arrayType.getElementType());
491 }
492 return false;
493 }
494
495 static LogicalResult emitAmbiguousContainmentRhs(EmitContainmentOp containOp, StringRef detail) {
496 return containOp.emitOpError()
497 << "poly lowering cannot resolve containment RHS row write history: " << detail;
498 }
499
501 template <typename IndexRange, typename PrefixRange>
502 static bool indexStartsWith(const IndexRange &index, const PrefixRange &prefix) {
503 auto indexIt = index.begin();
504 for (Attribute attr : prefix) {
505 if (indexIt == index.end() || *indexIt != attr) {
506 return false;
507 }
508 ++indexIt;
509 }
510 return true;
511 }
512
514 template <typename LhsRange, typename RhsRange>
515 static bool prefixesCanOverlap(const LhsRange &lhs, const RhsRange &rhs) {
516 auto lhsIt = lhs.begin();
517 auto rhsIt = rhs.begin();
518 while (lhsIt != lhs.end() && rhsIt != rhs.end()) {
519 if (*lhsIt != *rhsIt) {
520 return false;
521 }
522 ++lhsIt;
523 ++rhsIt;
524 }
525 return true;
526 }
527
529 static ArrayAttr dropIndexPrefix(MLIRContext *ctx, ArrayAttr index, size_t prefixSize) {
530 SmallVector<Attribute> attrs;
531 size_t idx = 0;
532 for (Attribute attr : index) {
533 if (idx++ >= prefixSize) {
534 attrs.push_back(attr);
535 }
536 }
537 return ArrayAttr::get(ctx, attrs);
538 }
539
541 template <typename PrefixRange>
542 static ArrayAttr appendIndex(MLIRContext *ctx, const PrefixRange &prefix, ArrayAttr suffix) {
543 SmallVector<Attribute> attrs;
544 for (Attribute attr : prefix) {
545 attrs.push_back(attr);
546 }
547 for (Attribute attr : suffix) {
548 attrs.push_back(attr);
549 }
550 return ArrayAttr::get(ctx, attrs);
551 }
552
554 static inline ArrayAttr getStaticAccessIndex(Operation *op) {
555 return llvm::cast<ArrayAccessOpInterface>(op).indexOperandsToAttributeArray();
556 }
557
560 static std::optional<SmallVector<ArrayAttr>>
561 getViewIndices(ArrayType arrayType, ArrayRef<Attribute> viewPrefix) {
562 std::optional<SmallVector<ArrayAttr>> allIndices = arrayType.getSubelementIndices();
563 if (!allIndices) {
564 return std::nullopt;
565 }
566
567 SmallVector<ArrayAttr> viewIndices;
568 MLIRContext *ctx = arrayType.getContext();
569 for (ArrayAttr index : *allIndices) {
570 if (indexStartsWith(index, viewPrefix)) {
571 viewIndices.push_back(dropIndexPrefix(ctx, index, viewPrefix.size()));
572 }
573 }
574 return viewIndices;
575 }
576
580 static LogicalResult collectMutableContainmentElements(
581 Value arrayValue, Operation *boundaryOp, ArrayRef<Attribute> viewPrefix,
582 EmitContainmentOp containOp, DenseSet<Value> &activeArrays,
583 SmallVectorImpl<MutableContainmentElement> &elements
584 ) {
585 auto arrayType = llvm::dyn_cast<ArrayType>(arrayValue.getType());
586 if (!arrayType || !llvm::isa<FeltType>(arrayType.getElementType())) {
587 return success();
588 }
589
590 DenseMap<Attribute, OpOperand *> finalElements;
591 if (failed(collectMutableContainmentElementMap(
592 arrayValue, boundaryOp, viewPrefix, containOp, activeArrays, finalElements
593 ))) {
594 return failure();
595 }
596
597 std::optional<SmallVector<ArrayAttr>> viewIndices = getViewIndices(arrayType, viewPrefix);
598 if (!viewIndices) {
599 if (finalElements.empty()) {
600 return success();
601 }
602 return emitAmbiguousContainmentRhs(containOp, "array shape is not static");
603 }
604
605 for (ArrayAttr relativeIndex : *viewIndices) {
606 auto elementIt = finalElements.find(relativeIndex);
607 if (elementIt != finalElements.end()) {
608 elements.push_back(MutableContainmentElement {relativeIndex, elementIt->second});
609 }
610 }
611 return success();
612 }
613
617 static std::optional<std::pair<Value, FlatSymbolRefAttr>> resolveStructReadSource(Value v) {
618 if (auto readOp = v.getDefiningOp<MemberReadOp>()) {
619 return std::make_pair(readOp.getComponent(), readOp.getMemberNameAttr());
620 }
621 return std::nullopt;
622 }
623
629 static bool mayAliasArraySource(Value a, Value b) {
630 if (a == b) {
631 return true;
632 }
633 auto srcA = resolveStructReadSource(a);
634 if (!srcA) {
635 return false;
636 }
637 auto srcB = resolveStructReadSource(b);
638 if (!srcB) {
639 return false;
640 }
641 return srcA->first == srcB->first && srcA->second == srcB->second;
642 }
643
647 static LogicalResult collectMutableContainmentElementMap(
648 Value arrayValue, Operation *boundaryOp, ArrayRef<Attribute> viewPrefix,
649 EmitContainmentOp containOp, DenseSet<Value> &activeArrays,
650 DenseMap<Attribute, OpOperand *> &finalElements
651 ) {
652 // Track the final visible felt-array operands at boundaryOp, keyed by
653 // indices relative to viewPrefix. Ambiguous histories fail instead of guessing.
654 auto arrayType = llvm::dyn_cast<ArrayType>(arrayValue.getType());
655 if (!arrayType || !llvm::isa<FeltType>(arrayType.getElementType())) {
656 return success();
657 }
658 if (!boundaryOp || !boundaryOp->getBlock()) {
659 return emitAmbiguousContainmentRhs(containOp, "missing observation block");
660 }
661 if (!activeArrays.insert(arrayValue).second) {
662 return emitAmbiguousContainmentRhs(containOp, "cyclic array update");
663 }
664 auto cleanup = llvm::make_scope_exit([&]() { activeArrays.erase(arrayValue); });
665
666 MLIRContext *ctx = arrayType.getContext();
667
668 if (auto arrayOp = arrayValue.getDefiningOp<CreateArrayOp>()) {
669 MutableOperandRange elementOperands = arrayOp.getElementsMutable();
670 if (!elementOperands.empty()) {
671 std::optional<SmallVector<ArrayAttr>> allIndices = arrayType.getSubelementIndices();
672 if (!allIndices) {
673 return emitAmbiguousContainmentRhs(containOp, "array.new shape is not static");
674 }
675 assert(allIndices->size() == elementOperands.size() && "array.new verifier mismatch");
676
677 auto *indexIt = allIndices->begin();
678 for (OpOperand &elementOperand : elementOperands) {
679 ArrayAttr index = *indexIt++;
680 if (indexStartsWith(index, viewPrefix)) {
681 finalElements[dropIndexPrefix(ctx, index, viewPrefix.size())] = &elementOperand;
682 }
683 }
684 }
685 } else if (auto extractOp = arrayValue.getDefiningOp<ExtractArrayOp>()) {
686 ArrayAttr extractIndex = getStaticAccessIndex(extractOp.getOperation());
687 if (!extractIndex) {
688 return emitAmbiguousContainmentRhs(containOp, "array.extract index is not static");
689 }
690
691 SmallVector<Attribute> sourcePrefix;
692 for (Attribute attr : extractIndex) {
693 sourcePrefix.push_back(attr);
694 }
695 for (Attribute attr : viewPrefix) {
696 sourcePrefix.push_back(attr);
697 }
698
699 if (failed(collectMutableContainmentElementMap(
700 extractOp.getArrRef(), extractOp.getOperation(), sourcePrefix, containOp,
701 activeArrays, finalElements
702 ))) {
703 return failure();
704 }
705 }
706
707 for (Operation &op : *boundaryOp->getBlock()) {
708 if (&op == boundaryOp) {
709 break;
710 }
711
712 if (auto writeOp = llvm::dyn_cast<WriteArrayOp>(&op)) {
713 if (!mayAliasArraySource(writeOp.getArrRef(), arrayValue)) {
714 continue;
715 }
716
717 ArrayAttr writeIndex = getStaticAccessIndex(writeOp.getOperation());
718 if (!writeIndex) {
719 return emitAmbiguousContainmentRhs(containOp, "array.write index is not static");
720 }
721 if (indexStartsWith(writeIndex, viewPrefix)) {
722 finalElements[dropIndexPrefix(ctx, writeIndex, viewPrefix.size())] =
723 &writeOp.getRvalueMutable();
724 }
725 continue;
726 }
727
728 if (auto insertOp = llvm::dyn_cast<InsertArrayOp>(&op)) {
729 if (!mayAliasArraySource(insertOp.getArrRef(), arrayValue)) {
730 continue;
731 }
732
733 ArrayAttr insertIndex = getStaticAccessIndex(insertOp.getOperation());
734 if (!insertIndex) {
735 return emitAmbiguousContainmentRhs(containOp, "array.insert index is not static");
736 }
737 if (!prefixesCanOverlap(insertIndex, viewPrefix)) {
738 continue;
739 }
740
741 auto rvalueType = llvm::dyn_cast<ArrayType>(insertOp.getRvalue().getType());
742 if (!rvalueType || !llvm::isa<FeltType>(rvalueType.getElementType())) {
743 continue;
744 }
745
746 std::optional<SmallVector<ArrayAttr>> rvalueIndices = rvalueType.getSubelementIndices();
747 if (!rvalueIndices) {
748 return emitAmbiguousContainmentRhs(containOp, "array.insert rvalue shape is not static");
749 }
750
751 SmallVector<MutableContainmentElement> insertedElements;
752 if (failed(collectMutableContainmentElements(
753 insertOp.getRvalue(), insertOp.getOperation(), ArrayRef<Attribute> {}, containOp,
754 activeArrays, insertedElements
755 ))) {
756 return failure();
757 }
758
759 DenseMap<Attribute, OpOperand *> insertedElementMap;
760 for (MutableContainmentElement element : insertedElements) {
761 insertedElementMap[element.index] = element.operand;
762 }
763
764 for (ArrayAttr rvalueIndex : *rvalueIndices) {
765 ArrayAttr targetIndex = appendIndex(ctx, insertIndex, rvalueIndex);
766 if (!indexStartsWith(targetIndex, viewPrefix)) {
767 continue;
768 }
769
770 ArrayAttr relativeIndex = dropIndexPrefix(ctx, targetIndex, viewPrefix.size());
771 auto elementIt = insertedElementMap.find(rvalueIndex);
772 if (elementIt == insertedElementMap.end()) {
773 finalElements.erase(relativeIndex);
774 continue;
775 }
776 finalElements[relativeIndex] = elementIt->second;
777 }
778 }
779 }
780
781 return success();
782 }
783
786 LogicalResult lowerContainmentRhsFeltOperand(
787 OpOperand &operand, StructDefOp structDef, FuncDefOp constrainFunc,
788 DominanceInfo &dominanceInfo, DenseMap<Value, unsigned> &degreeMemo,
789 DenseMap<Value, Value> &rewrites, SmallVector<AuxAssignment> &auxAssignments
790 ) {
791 Value value = operand.get();
792 if (!llvm::isa<FeltType>(value.getType())) {
793 return success();
794 }
795
796 unsigned degree = getDegree(value, degreeMemo);
797 if (degree > maxDegree) {
798 operand.set(lowerExpression(
799 value, structDef, constrainFunc, operand.getOwner(), dominanceInfo, degreeMemo, rewrites,
800 auxAssignments
801 ));
802 }
803 return success();
804 }
805
808 LogicalResult lowerContainmentRhsValue(
809 OpOperand &operand, StructDefOp structDef, FuncDefOp constrainFunc,
810 DominanceInfo &dominanceInfo, DenseMap<Value, unsigned> &degreeMemo,
811 DenseMap<Value, Value> &rewrites, SmallVector<AuxAssignment> &auxAssignments,
812 EmitContainmentOp containOp
813 ) {
814 Value value = operand.get();
815 if (llvm::isa<FeltType>(value.getType())) {
816 return lowerContainmentRhsFeltOperand(
817 operand, structDef, constrainFunc, dominanceInfo, degreeMemo, rewrites, auxAssignments
818 );
819 }
820
821 if (!isFeltArray(value.getType())) {
822 return success();
823 }
824
825 DenseSet<Value> activeArrays;
826 SmallVector<MutableContainmentElement> elements;
827 if (failed(collectMutableContainmentElements(
828 value, containOp.getOperation(), ArrayRef<Attribute> {}, containOp, activeArrays,
829 elements
830 ))) {
831 return failure();
832 }
833
834 for (MutableContainmentElement element : elements) {
835 if (failed(lowerContainmentRhsFeltOperand(
836 *element.operand, structDef, constrainFunc, dominanceInfo, degreeMemo, rewrites,
837 auxAssignments
838 ))) {
839 return failure();
840 }
841 }
842
843 return success();
844 }
845
848 LogicalResult checkContainmentRhsFeltValue(
849 Value value, EmitContainmentOp containOp, DenseMap<Value, unsigned> &checkMemo
850 ) {
851 if (!llvm::isa<FeltType>(value.getType())) {
852 return success();
853 }
854
855 unsigned valueDegree = getDegree(value, checkMemo);
856 if (valueDegree <= maxDegree) {
857 return success();
858 }
859
860 return containOp.emitOpError()
861 << "poly lowering postcondition failed: "
862 "containment RHS element degree exceeds max-degree "
863 << maxDegree.getValue() << " (element degree " << valueDegree << ')';
864 }
865
868 LogicalResult checkContainmentRhsValue(
869 Value value, EmitContainmentOp containOp, DenseMap<Value, unsigned> &checkMemo
870 ) {
871 if (llvm::isa<FeltType>(value.getType())) {
872 return checkContainmentRhsFeltValue(value, containOp, checkMemo);
873 }
874
875 if (!isFeltArray(value.getType())) {
876 return success();
877 }
878
879 DenseSet<Value> activeArrays;
880 SmallVector<MutableContainmentElement> elements;
881 auto res = collectMutableContainmentElements(
882 value, containOp.getOperation(), {}, containOp, activeArrays, elements
883 );
884 if (failed(res)) {
885 return failure();
886 }
887
888 for (MutableContainmentElement element : elements) {
889 if (failed(checkContainmentRhsFeltValue(element.operand->get(), containOp, checkMemo))) {
890 return failure();
891 }
892 }
893 return success();
894 }
895
898 LogicalResult checkContainmentRhsDegrees(FuncDefOp constrainFunc) {
899 auto res = constrainFunc.walk([&](EmitContainmentOp containOp) -> WalkResult {
900 DenseMap<Value, unsigned> memo;
901 return checkContainmentRhsValue(containOp.getRhs(), containOp, memo);
902 });
903 return failure(res.wasInterrupted());
904 }
905
906 LogicalResult lowerInConstrain(
907 StructDefOp structDef, FuncDefOp constrainFunc, SmallVector<AuxAssignment> &auxAssignments
908 ) {
909
910 DenseMap<Value, unsigned> degreeMemo;
911 DenseMap<Value, Value> rewrites;
912 DominanceInfo dominanceInfo(constrainFunc);
913
914 // Lower equality constraints
915 constrainFunc.walk([&](EmitEqualityOp constraintOp) {
916 if (!llvm::isa<FeltType>(constraintOp.getLhs().getType()) ||
917 !llvm::isa<FeltType>(constraintOp.getRhs().getType())) {
918 return;
919 }
920
921 auto &lhsOperand = constraintOp.getLhsMutable();
922 auto &rhsOperand = constraintOp.getRhsMutable();
923 unsigned degreeLhs = getDegree(lhsOperand.get(), degreeMemo);
924 unsigned degreeRhs = getDegree(rhsOperand.get(), degreeMemo);
925
926 if (degreeLhs > maxDegree) {
927 Value loweredExpr = lowerExpression(
928 lhsOperand.get(), structDef, constrainFunc, constraintOp.getOperation(), dominanceInfo,
929 degreeMemo, rewrites, auxAssignments
930 );
931 lhsOperand.set(loweredExpr);
932 }
933 if (degreeRhs > maxDegree) {
934 Value loweredExpr = lowerExpression(
935 rhsOperand.get(), structDef, constrainFunc, constraintOp.getOperation(), dominanceInfo,
936 degreeMemo, rewrites, auxAssignments
937 );
938 rhsOperand.set(loweredExpr);
939 }
940 });
941
942 // Lower containment lookup rows.
943 auto res = constrainFunc.walk([&](EmitContainmentOp containOp) -> WalkResult {
944 return lowerContainmentRhsValue(
945 containOp.getRhsMutable(), structDef, constrainFunc, dominanceInfo, degreeMemo, rewrites,
946 auxAssignments, containOp
947 );
948 });
949 if (res.wasInterrupted()) {
950 return failure();
951 }
952
953 // Lower function call arguments
954 constrainFunc.walk([&](CallOp callOp) {
955 if (callOp.calleeIsStructConstrain()) {
956 SmallVector<Value> newOperands = llvm::to_vector(callOp.getArgOperands());
957 bool modified = false;
958
959 for (Value &arg : newOperands) {
960 if (!llvm::isa<FeltType>(arg.getType())) {
961 continue;
962 }
963
964 DenseMap<Value, unsigned> callMemo;
965 if (getDegree(arg, callMemo) > 1) {
966 arg = materializeCallArgument(
967 arg, structDef, constrainFunc, callOp, dominanceInfo, degreeMemo, rewrites,
968 auxAssignments
969 );
970 modified = true;
971 }
972 }
973
974 if (modified) {
975 OpBuilder builder(callOp);
976 builder.create<CallOp>(
977 callOp.getLoc(), callOp.getResultTypes(), callOp.getCallee(),
979 newOperands
980 );
981 callOp->erase();
982 }
983 }
984 });
985
986 return success();
987 }
988
991 static LogicalResult
992 rebuildInCompute(FuncDefOp computeFunc, const SmallVector<AuxAssignment> &auxAssignments) {
993 DenseMap<Value, Value> rebuildMemo;
994 Block &computeBlock = computeFunc.getBody().front();
995 OpBuilder builder(&computeBlock, computeBlock.getTerminator()->getIterator());
996 Value selfVal = computeFunc.getSelfValueFromCompute();
997
998 SmallVector<unsigned> orderedAuxAssignments;
999 orderedAuxAssignments.reserve(auxAssignments.size());
1000 if (failed(orderAuxAssignments(auxAssignments, orderedAuxAssignments))) {
1001 return failure();
1002 }
1003
1004 for (unsigned assignIdx : orderedAuxAssignments) {
1005 const auto &assign = auxAssignments[assignIdx];
1006 Value rebuiltExpr =
1007 rebuildExprInCompute(assign.computedValue, computeFunc, builder, rebuildMemo);
1008 if (!rebuiltExpr) {
1009 return failure();
1010 }
1011 builder.create<MemberWriteOp>(
1012 assign.computedValue.getLoc(), selfVal, builder.getStringAttr(assign.auxMemberName),
1013 rebuiltExpr
1014 );
1015 if (assign.auxValue) {
1016 // Reuse the expression just written so later aux producers do not need an
1017 // immediate read from the generated aux member.
1018 rebuildMemo[assign.auxValue] = rebuiltExpr;
1019 }
1020 }
1021 return success();
1022 }
1023
1024 void runOnOperation() override {
1025 ModuleOp moduleOp = getOperation();
1026
1027 // Validate degree parameter
1028 if (maxDegree < 2) {
1029 moduleOp.emitError()
1030 .append("Invalid max degree: ", maxDegree.getValue(), ". Must be >= 2.")
1031 .report();
1032 signalPassFailure();
1033 return;
1034 }
1035
1036 auto moduleRes = moduleOp.walk([this](StructDefOp structDef) -> WalkResult {
1037 try {
1038 if (failed(checkForAuxMemberConflicts(structDef, AUXILIARY_MEMBER_PREFIX))) {
1039 return WalkResult::interrupt();
1040 }
1041
1042 FuncDefOp constrainFunc = structDef.getConstrainFuncOp();
1043 if (!constrainFunc) {
1044 return structDef.emitOpError() << '"' << structDef.getName() << "\" doesn't have a \"@"
1045 << FUNC_NAME_CONSTRAIN << "\" function";
1046 }
1047
1048 if (failed(checkFuncBodyIsStraightLine(constrainFunc, "poly lowering"))) {
1049 return WalkResult::interrupt();
1050 }
1051
1052 FuncDefOp computeFunc = structDef.getComputeFuncOp();
1053 if (!computeFunc) {
1054 return structDef.emitOpError() << '"' << structDef.getName() << "\" doesn't have a \"@"
1055 << FUNC_NAME_COMPUTE << "\" function";
1056 }
1057
1058 if (failed(checkFuncBodyIsStraightLine(computeFunc, "poly lowering"))) {
1059 return WalkResult::interrupt();
1060 }
1061
1062 SmallVector<AuxAssignment> auxAssignments;
1063 if (failed(lowerInConstrain(structDef, constrainFunc, auxAssignments))) {
1064 return WalkResult::interrupt();
1065 }
1066
1067 if (failed(checkEqualityDegrees(constrainFunc))) {
1068 return WalkResult::interrupt();
1069 }
1070
1071 if (failed(checkContainmentRhsDegrees(constrainFunc))) {
1072 return WalkResult::interrupt();
1073 }
1074
1075 if (failed(checkStructConstrainCallArguments(constrainFunc))) {
1076 return WalkResult::interrupt();
1077 }
1078
1079 if (failed(rebuildInCompute(computeFunc, auxAssignments))) {
1080 return WalkResult::interrupt();
1081 }
1082
1083 } catch (const DegreeComputationError &err) {
1084 mlir::emitError(err.getLoc()) << err.what();
1085 return WalkResult::interrupt();
1086 }
1087
1088 return WalkResult::advance();
1089 });
1090
1091 if (moduleRes.wasInterrupted()) {
1092 signalPassFailure();
1093 }
1094 }
1095};
1096
1097} // namespace
Shared utility function implementations for LLZK lowering passes.
#define AUXILIARY_MEMBER_PREFIX
std::optional<::llvm::SmallVector<::mlir::ArrayAttr > > getSubelementIndices() const
Return a list of all valid indices for this ArrayType.
Definition Types.cpp:113
::mlir::Type getElementType() const
::llzk::function::FuncDefOp getConstrainFuncOp()
Gets the FuncDefOp that defines the constrain function in this structure, if present,...
Definition Ops.cpp:468
::llzk::function::FuncDefOp getComputeFuncOp()
Gets the FuncDefOp that defines the compute function in this structure, if present,...
Definition Ops.cpp:464
::mlir::TypedValue<::mlir::Type > getRhs()
Definition Ops.h.inc:130
::mlir::OpOperand & getRhsMutable()
Definition Ops.h.inc:139
::mlir::OpOperand & getRhsMutable()
Definition Ops.h.inc:285
::mlir::TypedValue<::mlir::Type > getLhs()
Definition Ops.h.inc:272
::mlir::OpOperand & getLhsMutable()
Definition Ops.h.inc:280
::mlir::TypedValue<::mlir::Type > getRhs()
Definition Ops.h.inc:276
bool calleeIsStructConstrain()
Return true iff the callee function name is FUNC_NAME_CONSTRAIN within a StructDefOp.
Definition Ops.cpp:1195
::mlir::SymbolRefAttr getCallee()
Definition Ops.cpp.inc:470
::llvm::ArrayRef< int32_t > getNumDimsPerMap()
Definition Ops.cpp.inc:480
::mlir::Operation::operand_range getArgOperands()
Definition Ops.h.inc:266
static constexpr ::llvm::StringLiteral getOperationName()
Definition Ops.h.inc:255
::mlir::OperandRangeRange getMapOperands()
Definition Ops.h.inc:270
static ::llvm::SmallVector<::mlir::ValueRange > toVectorOfValueRange(::mlir::OperandRangeRange)
Allocate consecutive storage of the ValueRange instances in the parameter so it can be passed to the ...
Definition Ops.cpp:1236
::mlir::Value getSelfValueFromCompute()
Return the "self" value (i.e.
Definition Ops.cpp:481
::mlir::Value getSelfValueFromConstrain()
Return the "self" value (i.e.
Definition Ops.cpp:500
::mlir::Region & getBody()
Definition Ops.h.inc:703
::mlir::Pass::Option< unsigned > maxDegree
ExpressionValue add(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
constexpr char FUNC_NAME_COMPUTE[]
Symbol name for the witness generation (and resp.
Definition Constants.h:16
Value rebuildExprInCompute(Value val, FuncDefOp computeFunc, OpBuilder &builder, DenseMap< Value, Value > &memo)
void replaceSubsequentUsesWith(Value oldVal, Value newVal, Operation *afterOp)
constexpr char FUNC_NAME_CONSTRAIN[]
Definition Constants.h:17
ExpressionValue neg(const llvm::SMTSolverRef &solver, const ExpressionValue &val)
MemberDefOp addAuxMember(StructDefOp structDef, StringRef name, Type type)
ExpressionValue div(const llvm::SMTSolverRef &solver, Operation *op, const ExpressionValue &lhs, const ExpressionValue &rhs)
ExpressionValue mul(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
LogicalResult checkFuncBodyIsStraightLine(FuncDefOp func, StringRef passName)
ExpressionValue sub(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
LogicalResult checkForAuxMemberConflicts(StructDefOp structDef, StringRef prefix)