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