LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
LLZKLoweringUtils.cpp
Go to the documentation of this file.
1//===-- LLZKLoweringUtils.cpp -----------------------------------*- C++ -*-===//
2//
3// Shared utility function implementations for LLZK lowering passes.
4//
5//===----------------------------------------------------------------------===//
6
8
10
11#include <mlir/IR/Block.h>
12#include <mlir/IR/Builders.h>
13#include <mlir/IR/BuiltinOps.h>
14#include <mlir/IR/IRMapping.h>
15#include <mlir/IR/Operation.h>
16#include <mlir/IR/SymbolTable.h>
17#include <mlir/Support/LogicalResult.h>
18
19#include <llvm/ADT/STLExtras.h>
20#include <llvm/ADT/SmallVector.h>
21#include <llvm/Support/raw_ostream.h>
22
23using namespace mlir;
24using namespace llzk;
25using namespace llzk::felt;
26using namespace llzk::function;
27using namespace llzk::component;
28using namespace llzk::constrain;
29
30namespace llzk {
31
32namespace {
33
34Value mapBlockArgumentInCompute(BlockArgument barg, FuncDefOp computeFunc) {
35 // Constrain entry arguments map onto compute inputs: constrain(%self, args...)
36 // corresponds to compute(args...), plus the compute-side `%self`.
37 if (barg.getArgNumber() == 0) {
38 return computeFunc.getSelfValueFromCompute();
39 }
40 return computeFunc.getArgument(barg.getArgNumber() - 1);
41}
42
43Value mapValueIntoCompute(
44 Value val, FuncDefOp computeFunc, OpBuilder &builder, DenseMap<Value, Value> &memo
45) {
46 if (auto it = memo.find(val); it != memo.end()) {
47 return it->second;
48 }
49 if (auto barg = llvm::dyn_cast<BlockArgument>(val)) {
50 return memo[val] = mapBlockArgumentInCompute(barg, computeFunc);
51 }
52 return rebuildExprInCompute(val, computeFunc, builder, memo);
53}
54
55} // namespace
56
58 Value val, FuncDefOp computeFunc, OpBuilder &builder, DenseMap<Value, Value> &memo
59) {
60 if (auto it = memo.find(val); it != memo.end()) {
61 return it->second;
62 }
63
64 if (auto barg = llvm::dyn_cast<BlockArgument>(val)) {
65 return memo[val] = mapBlockArgumentInCompute(barg, computeFunc);
66 }
67
68 if (auto readOp = val.getDefiningOp<MemberReadOp>()) {
69 IRMapping mapper;
70 for (Value operand : readOp->getOperands()) {
71 Value rebuiltOperand = mapValueIntoCompute(operand, computeFunc, builder, memo);
72 if (!rebuiltOperand) {
73 return nullptr;
74 }
75 mapper.map(operand, rebuiltOperand);
76 }
77
78 Operation *rebuiltOp = builder.clone(*readOp.getOperation(), mapper);
79 assert(rebuiltOp->getNumResults() == 1 && "member reads have exactly one result");
80 return memo[val] = rebuiltOp->getResult(0);
81 }
82
83 if (auto callOp = val.getDefiningOp<CallOp>()) {
84 if (!callOp.getMapOperands().empty()) {
85 callOp
86 .emitError(
87 "cannot rebuild affine-instantiated function.call in compute-side auxiliary "
88 "expression"
89 )
90 .report();
91 return nullptr;
92 }
93
94 SymbolTableCollection tables;
95 FailureOr<SymbolLookupResult<FuncDefOp>> target = callOp.getCalleeTarget(tables);
96 if (failed(target)) {
97 return nullptr;
98 }
99 FuncDefOp targetFunc = target->get();
100 bool invalidTarget =
101 (targetFunc.hasAllowConstraintAttr() && !computeFunc.hasAllowConstraintAttr()) ||
102 (targetFunc.hasAllowWitnessAttr() && !computeFunc.hasAllowWitnessAttr()) ||
103 (targetFunc.hasAllowNonNativeFieldOpsAttr() &&
104 !computeFunc.hasAllowNonNativeFieldOpsAttr());
105 if (invalidTarget) {
106 callOp
107 .emitError(
108 "cannot rebuild function.call in compute-side auxiliary expression: callee "
109 "requires attributes not present on the compute function"
110 )
111 .report();
112 return nullptr;
113 }
114
115 SmallVector<Value> rebuiltArgs;
116 rebuiltArgs.reserve(callOp.getArgOperands().size());
117 for (Value arg : callOp.getArgOperands()) {
118 Value rebuiltArg = rebuildExprInCompute(arg, computeFunc, builder, memo);
119 if (!rebuiltArg) {
120 return nullptr;
121 }
122 rebuiltArgs.push_back(rebuiltArg);
123 }
124
125 ArrayRef<Attribute> templateParams;
126 if (ArrayAttr params = callOp.getTemplateParamsAttr()) {
127 templateParams = params.getValue();
128 }
129
130 CallOp rebuilt = builder.create<CallOp>(
131 callOp.getLoc(), callOp.getResultTypes(), callOp.getCalleeAttr(), rebuiltArgs,
132 templateParams
133 );
134 for (auto [oldResult, newResult] : llvm::zip(callOp.getResults(), rebuilt.getResults())) {
135 memo[oldResult] = newResult;
136 }
137 return memo[val];
138 }
139
140 if (val.getType().isIndex()) {
141 // Preserve index producers used by member-read access operands so rebuilt reads
142 // keep the original access semantics.
143 Operation *defOp = val.getDefiningOp();
144 assert(defOp && "index block arguments should already be mapped");
145
146 IRMapping mapper;
147 for (Value operand : defOp->getOperands()) {
148 Value rebuiltOperand = mapValueIntoCompute(operand, computeFunc, builder, memo);
149 if (!rebuiltOperand) {
150 return nullptr;
151 }
152 mapper.map(operand, rebuiltOperand);
153 }
154
155 Operation *rebuiltOp = builder.clone(*defOp, mapper);
156 assert(
157 rebuiltOp->getNumResults() == defOp->getNumResults() &&
158 "cloned index op should preserve result count"
159 );
160 unsigned resultNumber = llvm::cast<OpResult>(val).getResultNumber();
161 return memo[val] = rebuiltOp->getResult(resultNumber);
162 }
163
164 if (auto add = val.getDefiningOp<AddFeltOp>()) {
165 Value lhs = rebuildExprInCompute(add.getLhs(), computeFunc, builder, memo);
166 Value rhs = rebuildExprInCompute(add.getRhs(), computeFunc, builder, memo);
167 if (!lhs || !rhs) {
168 return nullptr;
169 }
170 return memo[val] = builder.create<AddFeltOp>(add.getLoc(), add.getType(), lhs, rhs);
171 }
172
173 if (auto sub = val.getDefiningOp<SubFeltOp>()) {
174 Value lhs = rebuildExprInCompute(sub.getLhs(), computeFunc, builder, memo);
175 Value rhs = rebuildExprInCompute(sub.getRhs(), computeFunc, builder, memo);
176 if (!lhs || !rhs) {
177 return nullptr;
178 }
179 return memo[val] = builder.create<SubFeltOp>(sub.getLoc(), sub.getType(), lhs, rhs);
180 }
181
182 if (auto mul = val.getDefiningOp<MulFeltOp>()) {
183 Value lhs = rebuildExprInCompute(mul.getLhs(), computeFunc, builder, memo);
184 Value rhs = rebuildExprInCompute(mul.getRhs(), computeFunc, builder, memo);
185 if (!lhs || !rhs) {
186 return nullptr;
187 }
188 return memo[val] = builder.create<MulFeltOp>(mul.getLoc(), mul.getType(), lhs, rhs);
189 }
190
191 if (auto neg = val.getDefiningOp<NegFeltOp>()) {
192 Value operand = rebuildExprInCompute(neg.getOperand(), computeFunc, builder, memo);
193 if (!operand) {
194 return nullptr;
195 }
196 return memo[val] = builder.create<NegFeltOp>(neg.getLoc(), neg.getType(), operand);
197 }
198
199 if (auto div = val.getDefiningOp<DivFeltOp>()) {
200 Value lhs = rebuildExprInCompute(div.getLhs(), computeFunc, builder, memo);
201 Value rhs = rebuildExprInCompute(div.getRhs(), computeFunc, builder, memo);
202 if (!lhs || !rhs) {
203 return nullptr;
204 }
205 return memo[val] = builder.create<DivFeltOp>(div.getLoc(), div.getType(), lhs, rhs);
206 }
207
208 if (auto c = val.getDefiningOp<FeltConstantOp>()) {
209 return memo[val] = builder.create<FeltConstantOp>(c.getLoc(), c.getValueAttr());
210 }
211
212 if (Operation *op = val.getDefiningOp()) {
213 op->emitError("cannot rebuild unsupported operation in compute-side auxiliary expression")
214 .report();
215 }
216 return nullptr;
217}
218
219LogicalResult checkForAuxMemberConflicts(StructDefOp structDef, StringRef prefix) {
220 bool conflictFound = false;
221
222 structDef.walk([&conflictFound, &prefix](MemberDefOp memberDefOp) {
223 if (memberDefOp.getName().starts_with(prefix)) {
224 (memberDefOp.emitError() << "Member name '" << memberDefOp.getName()
225 << "' conflicts with reserved prefix '" << prefix << '\'')
226 .report();
227 conflictFound = true;
228 }
229 });
230
231 return failure(conflictFound);
232}
233
234LogicalResult checkFuncBodyIsStraightLine(FuncDefOp func, StringRef passName) {
235 StringRef funcName = "function";
236 if (func.isStructCompute()) {
237 funcName = "compute";
238 } else if (func.isStructConstrain()) {
239 funcName = "constrain";
240 }
241
242 auto emitStraightLineError = [passName, funcName](Operation *op) {
243 op->emitError() << passName << " expects a straight-line " << funcName
244 << " body; run `llzk-flatten` or another control-flow lowering pass first";
245 };
246
247 Region &body = func.getBody();
248 if (!body.hasOneBlock()) {
249 emitStraightLineError(func.getOperation());
250 return failure();
251 }
252
253 Operation *unsupportedControlFlowOp = nullptr;
254 body.walk([&](Operation *op) {
255 if (op->getNumRegions() != 0 || op->getNumSuccessors() != 0) {
256 unsupportedControlFlowOp = op;
257 return WalkResult::interrupt();
258 }
259 return WalkResult::advance();
260 });
261
262 if (!unsupportedControlFlowOp) {
263 return success();
264 }
265
266 emitStraightLineError(unsupportedControlFlowOp);
267 return failure();
268}
269
270void replaceSubsequentUsesWith(Value oldVal, Value newVal, Operation *afterOp) {
271 assert(afterOp && "afterOp must be a valid Operation*");
272
273 for (auto &use : llvm::make_early_inc_range(oldVal.getUses())) {
274 Operation *user = use.getOwner();
275
276 // Skip uses that are:
277 // - Before afterOp in the same block.
278 // - Inside afterOp itself.
279 if ((user->getBlock() == afterOp->getBlock()) &&
280 (user == afterOp || user->isBeforeInBlock(afterOp))) {
281 continue;
282 }
283
284 // Replace this use of oldVal with newVal.
285 use.set(newVal);
286 }
287}
288
289MemberDefOp addAuxMember(StructDefOp structDef, StringRef name, Type type) {
290 assert(type && "auxiliary member type must be non-null");
291
292 OpBuilder builder(structDef);
293 builder.setInsertionPointToEnd(structDef.getBody());
294 return builder.create<MemberDefOp>(structDef.getLoc(), builder.getStringAttr(name), type);
295}
296
297unsigned getFeltDegree(Value val, DenseMap<Value, unsigned> &memo) {
298 if (auto it = memo.find(val); it != memo.end()) {
299 return it->second;
300 }
301
302 if (isa<FeltConstantOp>(val.getDefiningOp())) {
303 return memo[val] = 0;
304 }
305 if (isa<NonDetOp, MemberReadOp>(val.getDefiningOp()) || isa<BlockArgument>(val)) {
306 return memo[val] = 1;
307 }
308 if (auto add = val.getDefiningOp<AddFeltOp>()) {
309 return memo[val] =
310 std::max(getFeltDegree(add.getLhs(), memo), getFeltDegree(add.getRhs(), memo));
311 }
312 if (auto sub = val.getDefiningOp<SubFeltOp>()) {
313 return memo[val] =
314 std::max(getFeltDegree(sub.getLhs(), memo), getFeltDegree(sub.getRhs(), memo));
315 }
316 if (auto mul = val.getDefiningOp<MulFeltOp>()) {
317 return memo[val] = getFeltDegree(mul.getLhs(), memo) + getFeltDegree(mul.getRhs(), memo);
318 }
319 if (auto div = val.getDefiningOp<DivFeltOp>()) {
320 return memo[val] = getFeltDegree(div.getLhs(), memo) + getFeltDegree(div.getRhs(), memo);
321 }
322 if (auto neg = val.getDefiningOp<NegFeltOp>()) {
323 return memo[val] = getFeltDegree(neg.getOperand(), memo);
324 }
325
326 llvm::errs() << "Unhandled felt op in degree computation: " << val << '\n';
327 llvm_unreachable("Unhandled op in getFeltDegree");
328}
329
330} // namespace llzk
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
Definition LICENSE.txt:9
::mlir::Value getSelfValueFromCompute()
Return the "self" value (i.e.
Definition Ops.cpp:473
bool hasAllowNonNativeFieldOpsAttr()
Return true iff the function def has the allow_non_native_field_ops attribute.
Definition Ops.h.inc:828
bool hasAllowWitnessAttr()
Return true iff the function def has the allow_witness attribute.
Definition Ops.h.inc:820
bool isStructCompute()
Return true iff the function is within a StructDefOp and named FUNC_NAME_COMPUTE.
Definition Ops.h.inc:899
bool isStructConstrain()
Return true iff the function is within a StructDefOp and named FUNC_NAME_CONSTRAIN.
Definition Ops.h.inc:902
::mlir::Region & getBody()
Definition Ops.h.inc:698
bool hasAllowConstraintAttr()
Return true iff the function def has the allow_constraint attribute.
Definition Ops.h.inc:812
ExpressionValue add(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
Value rebuildExprInCompute(Value val, FuncDefOp computeFunc, OpBuilder &builder, DenseMap< Value, Value > &memo)
void replaceSubsequentUsesWith(Value oldVal, Value newVal, Operation *afterOp)
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)
unsigned getFeltDegree(Value val, DenseMap< Value, unsigned > &memo)
LogicalResult checkForAuxMemberConflicts(StructDefOp structDef, StringRef prefix)