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