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