Add pass to algebraize felt operations - #640
Conversation
db87d75 to
a3adba9
Compare
a3adba9 to
b9540cf
Compare
|
@codex review |
ReviewNo blocking issues found in the proposed algebraization pass. The implementation consistently limits rewriting to constraint-bearing functions, handles unsupported unsigned division/modulo inputs with explicit diagnostics, and avoids caching full-width bit decompositions that could represent non-canonical field values. lib/Transforms/LLZKAlgebraizeFeltOpsPass.cppL353-L364 lib/Transforms/LLZKAlgebraizeFeltOpsPass.cppL571-L590 lib/Transforms/LLZKAlgebraizeFeltOpsPass.cppL665-L680 I did not modify the branch, so no commit or follow-up pull request was created. Checks
|
| const llvm::APInt &ConstrainAlgebraizer::primeOf(Type type) { | ||
| auto [it, inserted] = primes.try_emplace(type); | ||
| if (inserted) { | ||
| FeltType feltType = llvm::cast<FeltType>(type); |
There was a problem hiding this comment.
This cast might fail if the type passed in isn't actually a !felt.type, right? Can we have this method just take a FeltType and require the caller to check the cast instead?
There was a problem hiding this comment.
Similar comments elsewhere llvm::cast<FeltType>(...) is used
| %self = struct.new : <@MixedXor> | ||
| function.return %self : !struct.type<@MixedXor> | ||
| } | ||
| function.def @constrain(%self: !struct.type<@MixedXor>, %a: !F, %b: !F) attributes {function.allow_constraint, function.allow_non_native_field_ops} { |
There was a problem hiding this comment.
Since this pass removes all the operations enabled by this attribute, we could also remove it from the function.
| @@ -0,0 +1,29 @@ | |||
| // RUN: llzk-opt -llzk-algebraize-felt-ops %s > /dev/null | |||
| const llvm::APInt &ConstrainAlgebraizer::primeOf(Type type) { | ||
| auto [it, inserted] = primes.try_emplace(type); | ||
| if (inserted) { | ||
| FeltType feltType = llvm::cast<FeltType>(type); |
| for (unsigned i = 0, e = sBits.size(); i < e; ++i) { | ||
| llvm::APInt cMinus1 = toExactWidthAPInt(curPow2 - 1, constBits); | ||
| Value scaled = b.create<MulFeltOp>(loc, sBits[i], feltConst(loc, type, cMinus1)); |
There was a problem hiding this comment.
nit: i is not used anywhere else in this loop
| for (unsigned i = 0, e = sBits.size(); i < e; ++i) { | |
| llvm::APInt cMinus1 = toExactWidthAPInt(curPow2 - 1, constBits); | |
| Value scaled = b.create<MulFeltOp>(loc, sBits[i], feltConst(loc, type, cMinus1)); | |
| for (auto &sBit : sBits) { | |
| llvm::APInt cMinus1 = toExactWidthAPInt(curPow2 - 1, constBits); | |
| Value scaled = b.create<MulFeltOp>(loc, sBit, feltConst(loc, type, cMinus1)); |
| /// Records the bound proven by an asserted range guard | ||
| /// `constrain.eq(cast.tofelt(bool.cmp lt/le/gt/ge(x, c)), 1)` — the pattern | ||
| /// frontends emit to pin blackbox input widths. Only guards at the top level | ||
| /// of the body may be harvested: a guard inside a conditional region holds | ||
| /// only on its branch. |
There was a problem hiding this comment.
What's the purpose of these guards in relation to the ops converted by this pass? Because, when converting an op inside a conditional block, wouldn't we be missing a range constraint for that op if it was also defined inside the same block?
| for (unsigned i = 1, e = bits.size(); i < e; ++i) { | ||
| Value term = b.create<MulFeltOp>(loc, bits[i], feltConst(loc, type, weight)); |
There was a problem hiding this comment.
nit: similar comment about i
| for (unsigned i = 1, e = bits.size(); i < e; ++i) { | |
| Value term = b.create<MulFeltOp>(loc, bits[i], feltConst(loc, type, weight)); | |
| for (auto &bit : bits.drop_front()) { | |
| Value term = b.create<MulFeltOp>(loc, bit, feltConst(loc, type, weight)); |
| for (Operation &op : fn.getBody().front()) { | ||
| if (auto eq = llvm::dyn_cast<EmitEqualityOp>(&op)) { | ||
| harvestRangeGuard(eq.getLhs(), eq.getRhs()); | ||
| harvestRangeGuard(eq.getRhs(), eq.getLhs()); | ||
| } | ||
| } |
There was a problem hiding this comment.
| for (Operation &op : fn.getBody().front()) { | |
| if (auto eq = llvm::dyn_cast<EmitEqualityOp>(&op)) { | |
| harvestRangeGuard(eq.getLhs(), eq.getRhs()); | |
| harvestRangeGuard(eq.getRhs(), eq.getLhs()); | |
| } | |
| } | |
| fn.walk([this](EmitEqualityOp eq) { | |
| harvestRangeGuard(eq.getLhs(), eq.getRhs()); | |
| harvestRangeGuard(eq.getRhs(), eq.getLhs()); | |
| }); |
| // rewritten op's result bound (set by cacheResultBits) feeds later ops. | ||
| // The snapshot keeps the loop off the ops the rewrites create. | ||
| SmallVector<Operation *> ops; | ||
| fn.walk([&](Operation *op) { ops.push_back(op); }); |
There was a problem hiding this comment.
nit: prefer to pass only the bindings actually used by the lambda
| fn.walk([&](Operation *op) { ops.push_back(op); }); | |
| fn.walk([&ops](Operation *op) { ops.push_back(op); }); |
| // One pass in program order: operands are defined before uses, so bounds | ||
| // are propagated before the ops consuming them are rewritten, and a | ||
| // rewritten op's result bound (set by cacheResultBits) feeds later ops. | ||
| // The snapshot keeps the loop off the ops the rewrites create. | ||
| SmallVector<Operation *> ops; | ||
| fn.walk([&](Operation *op) { ops.push_back(op); }); | ||
|
|
||
| for (Operation *op : ops) { |
There was a problem hiding this comment.
Instead of iterating over all the operations twice you could run the algorithm in the first pass, accumulating the to-be-removed ops along with their replacement. Then do another loop that actually removes and replaces the ops.
| }]; | ||
| } | ||
|
|
||
| def AlgebraizeFeltOpsPass : LLZKPass<"llzk-algebraize-felt-ops"> { |
There was a problem hiding this comment.
The pass could be made an operation pass for function.def. We don't need a whole-world pass for this and will allow running in parallel.
Implements stage 2 of #585.
Adds
-llzk-algebraize-felt-ops, an LLZK pre-lowering pass for functions markedfunction.allow_constraint. It lowers unsupported felt bitwise, shift, inverse, division, unsigned division, and modulo operations into basic felt arithmetic, nondeterministic witnesses, bit decompositions, and equality constraints.The pass reuses decompositions, uses known width bounds where available, and rejects unsupported division/modulo cases rather than producing unsound constraints.
Tested with the AlgebraizeFeltOps and PCLLowering lit suites (15 tests).