LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
LLZKComputeConstrainToProductPass.cpp
Go to the documentation of this file.
1//===-- LLZKComputeConstrainToProductPass.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// SPDX-License-Identifier: Apache-2.0
7//
8//===----------------------------------------------------------------------===//
13//===----------------------------------------------------------------------===//
14
16
22#include "llzk/Util/Constants.h"
24
25#include <mlir/IR/Builders.h>
26#include <mlir/IR/SymbolTable.h>
27#include <mlir/Transforms/Inliner.h>
28#include <mlir/Transforms/InliningUtils.h>
29
30#include <llvm/Support/Debug.h>
31
32#include <memory>
33#include <ranges>
34
35namespace llzk {
36#define GEN_PASS_DEF_COMPUTECONSTRAINTOPRODUCTPASS
38} // namespace llzk
39
40#define DEBUG_TYPE "llzk-compute-constrain-to-product-pass"
41
42using namespace mlir;
43using namespace llzk;
44using namespace llzk::component;
45using namespace llzk::function;
49 if (auto prod = root.getProductFuncOp()) {
50 return prod;
51 }
52
53 OpBuilder funcBuilder(compute);
55 // Add compute/constrain attributes
56 compute.walk([&funcBuilder](Operation *op) {
57 op->setAttr(PRODUCT_SOURCE, funcBuilder.getStringAttr(FUNC_NAME_COMPUTE));
58 });
60 constrain.walk([&funcBuilder](Operation *op) {
61 op->setAttr(PRODUCT_SOURCE, funcBuilder.getStringAttr(FUNC_NAME_CONSTRAIN));
62 });
63
64 // Create an empty @product func...
66 funcBuilder, funcBuilder.getFusedLoc({compute.getLoc(), constrain.getLoc()}),
68 );
69 productFunc->setAttr(DERIVED_ATTR_NAME, UnitAttr::get(funcBuilder.getContext()));
70 Block *entryBlock = productFunc.addEntryBlock();
71 funcBuilder.setInsertionPointToStart(entryBlock);
72
74 compute.hasAllowNonNativeFieldOpsAttr() || constrain.hasAllowNonNativeFieldOpsAttr()
75 );
76
77 // ...with the right arguments
78 llvm::SmallVector<Value> args {productFunc.getArguments()};
79
80 // Add calls to @compute and @constrain...
81 CallOp computeCall = CallOp::create(funcBuilder, funcBuilder.getUnknownLoc(), compute, args);
82 args.insert(args.begin(), computeCall->getResult(0));
83 CallOp constrainCall = CallOp::create(funcBuilder, funcBuilder.getUnknownLoc(), constrain, args);
84 ReturnOp::create(funcBuilder, funcBuilder.getUnknownLoc(), computeCall->getResult(0));
85
86 // ..and inline them
87 InlinerInterface inliner(productFunc.getContext());
88 InlinerConfig inlinerConfig;
89 if (failed(inlineCall(
90 inliner, inlinerConfig.getCloneCallback(), computeCall, compute, &compute.getBody(), true
91 ))) {
92 root->emitError().append("failed to inline ", FUNC_NAME_COMPUTE).report();
93 return nullptr;
94 }
95 if (failed(inlineCall(
96 inliner, inlinerConfig.getCloneCallback(), constrainCall, constrain, &constrain.getBody(),
97 true
98 ))) {
99 root->emitError().append("failed to inline ", FUNC_NAME_CONSTRAIN).report();
100 return nullptr;
101 }
102 computeCall->erase();
103 constrainCall->erase();
104
105 // Mark the compute/constrain for deletion
106 alignedStructs.push_back(root);
107
108 // Make sure we can align sub-calls to @compute and @constrain
109 if (failed(alignCalls(productFunc))) {
110 return nullptr;
111 }
112 return productFunc;
113}
114
115LogicalResult ProductAligner::alignCalls(FuncDefOp product) {
116 // Gather up all the remaining calls to @compute and @constrain
117 llvm::SetVector<CallOp> computeCalls, constrainCalls;
118 product.walk([&](CallOp callOp) {
119 if (callOp.calleeIsStructCompute()) {
120 computeCalls.insert(callOp);
121 } else if (callOp.calleeIsStructConstrain()) {
122 constrainCalls.insert(callOp);
123 }
124 });
125
126 llvm::SetVector<std::pair<CallOp, CallOp>> alignedCalls;
127
128 // A @compute matches a @constrain if they belong to the same struct and all their input signals
129 // are pairwise equivalent
130 auto doCallsMatch = [&](CallOp compute, CallOp constrain) -> bool {
131 LLVM_DEBUG({
132 llvm::outs() << "Asking for equivalence between calls\n"
133 << compute << "\nand\n"
134 << constrain << "\n\n";
135 llvm::outs() << "In block:\n\n" << *compute->getBlock() << '\n';
136 });
137
138 auto computeStruct = getPrefixAsSymbolRefAttr(compute.getCallee());
139 auto constrainStruct = getPrefixAsSymbolRefAttr(constrain.getCallee());
140 if (computeStruct != constrainStruct) {
141 return false;
142 }
143 if (compute.getNumOperands() == 0) {
144 return true;
145 }
146 for (unsigned i = 0, e = compute->getNumOperands() - 1; i < e; i++) {
147 if (!equivalence.areSignalsEquivalent(compute->getOperand(i), constrain->getOperand(i + 1))) {
148 return false;
149 }
150 }
151
152 return true;
153 };
154
155 for (auto compute : computeCalls) {
156 // If there is exactly one @compute that matches a given @constrain, we can align them
157 auto matches = llvm::filter_to_vector(constrainCalls, [&](CallOp constrain) {
158 return doCallsMatch(compute, constrain);
159 });
160
161 if (matches.size() == 1) {
162 alignedCalls.insert({compute, matches[0]});
163 computeCalls.remove(compute);
164 constrainCalls.remove(matches[0]);
165 }
166 }
167
168 if (!computeCalls.empty() && constrainCalls.empty()) {
169 product.emitWarning()
170 .append("failed to align some @", FUNC_NAME_COMPUTE, " and @", FUNC_NAME_CONSTRAIN)
171 .report();
172 }
173
174 for (auto [compute, constrain] : alignedCalls) {
175 // If @A::@compute matches @A::@constrain, recursively align the functions in @A...
176 auto calleeTgt = compute.getCalleeTarget(tables);
177 if (failed(calleeTgt)) {
178 return failure();
179 }
180 auto newRoot = calleeTgt->get()->getParentOfType<StructDefOp>();
181 assert(newRoot);
182 FuncDefOp newProduct =
183 alignFuncs(newRoot, newRoot.getComputeFuncOp(), newRoot.getConstrainFuncOp());
184 if (!newProduct) {
185 return failure();
186 }
187
188 // ...and replace the two calls with a single call to @A::@product
189 OpBuilder callBuilder(compute);
190 CallOp newCall = CallOp::create(
191 callBuilder, callBuilder.getFusedLoc({compute.getLoc(), constrain.getLoc()}), newProduct,
192 compute.getOperands()
193 );
194 compute->replaceAllUsesWith(newCall.getResults());
195 }
196
197 return success();
198}
199
200namespace {
201
202bool isValidRoot(StructDefOp root) {
203 FuncDefOp computeFunc = root.getComputeFuncOp();
204 FuncDefOp constrainFunc = root.getConstrainFuncOp();
205
206 if (!computeFunc || !constrainFunc) {
207 root->emitError()
208 .append("no ", FUNC_NAME_COMPUTE, "/", FUNC_NAME_CONSTRAIN, " to align")
209 .report();
210 return false;
211 }
212
215
216 return true;
217}
218
219LogicalResult alignStartingAt(
220 component::StructDefOp root, SymbolTableCollection &tables,
222) {
223 if (!isValidRoot(root)) {
224 return failure();
225 }
226
227 ProductAligner aligner {tables, equivalence};
228 if (!aligner.alignFuncs(root, root.getComputeFuncOp(), root.getConstrainFuncOp())) {
229 return failure();
230 }
231
232 return success();
233}
234
235class PassImpl : public llzk::impl::ComputeConstrainToProductPassBase<PassImpl> {
236 using Base = ComputeConstrainToProductPassBase<PassImpl>;
237 using Base::Base;
238
239 void runOnOperation() override {
240 ModuleOp mod = getOperation();
241 StructDefOp root;
242
243 SymbolTableCollection tables;
244 LightweightSignalEquivalenceAnalysis equivalence {
245 getAnalysis<LightweightSignalEquivalenceAnalysis>()
246 };
247
248 // Find the indicated root struct and make sure its a valid place to start aligning
249 mod.walk([&root, this](StructDefOp structDef) {
250 if (structDef.getSymName() == rootStruct) {
251 root = structDef;
252 }
253 });
254
255 if (!root) {
256 mod.emitError().append("could not find root struct \"", rootStruct, "\"").report();
257 signalPassFailure();
258 return;
259 }
260
261 if (failed(alignStartingAt(root, tables, equivalence))) {
262 signalPassFailure();
263 }
264 }
265};
266
267} // namespace
std::vector< component::StructDefOp > alignedStructs
function::FuncDefOp alignFuncs(component::StructDefOp root, function::FuncDefOp compute, function::FuncDefOp constrain)
mlir::LogicalResult alignCalls(function::FuncDefOp product)
::llvm::StringRef getSymName()
Definition Ops.cpp.inc:1910
::llzk::function::FuncDefOp getProductFuncOp()
Gets the FuncDefOp that defines the product function in this structure, if present,...
Definition Ops.cpp:474
::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
bool calleeIsStructConstrain()
Return true iff the callee function name is FUNC_NAME_CONSTRAIN within a StructDefOp.
Definition Ops.cpp:1004
bool calleeIsStructCompute()
Return true iff the callee function name is FUNC_NAME_COMPUTE within a StructDefOp.
Definition Ops.cpp:992
::mlir::SymbolRefAttr getCallee()
Definition Ops.cpp.inc:566
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
::mlir::FailureOr<::llzk::SymbolLookupResult<::llzk::function::FuncDefOp > > getCalleeTarget(::mlir::SymbolTableCollection &tables)
Resolve and return the target FuncDefOp for this CallOp.
Definition Ops.cpp:1037
static FuncDefOp create(::mlir::OpBuilder &builder, ::mlir::Location location, ::llvm::StringRef name, ::mlir::FunctionType type, ::llvm::ArrayRef<::mlir::NamedAttribute > attrs={}, ::llvm::ArrayRef<::mlir::DictionaryAttr > argAttrs={})
Definition Ops.cpp.inc:1252
::mlir::FunctionType getFunctionType()
Definition Ops.cpp.inc:1229
void setAllowNonNativeFieldOpsAttr(bool newValue=true)
Add (resp. remove) the allow_non_native_field_ops attribute to (resp. from) the function def.
Definition Ops.cpp:290
bool hasAllowNonNativeFieldOpsAttr()
Return true iff the function def has the allow_non_native_field_ops attribute.
Definition Ops.h.inc:1017
::mlir::Region & getBody()
Definition Ops.h.inc:871
static ReturnOp create(::mlir::OpBuilder &builder, ::mlir::Location location)
Definition Ops.cpp.inc:1666
constexpr char FUNC_NAME_COMPUTE[]
Symbol name for the witness generation (and resp.
Definition Constants.h:16
mlir::SymbolRefAttr getPrefixAsSymbolRefAttr(mlir::SymbolRefAttr symbol)
Return SymbolRefAttr like the one given but with the leaf/final element removed.
ExpressionValue mod(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
constexpr char PRODUCT_SOURCE[]
Name of the attribute on aligned product program ops that specifies where they came from.
Definition Constants.h:40
constexpr char FUNC_NAME_CONSTRAIN[]
Definition Constants.h:17
constexpr char FUNC_NAME_PRODUCT[]
Definition Constants.h:18
constexpr char DERIVED_ATTR_NAME[]
Name of the attribute on a @product func that has been automatically aligned from @compute + @constra...
Definition Constants.h:28