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