LLZK 2.1.1
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
ForbiddenPreconditionInfluence.cpp
Go to the documentation of this file.
1//===-- ForbiddenPreconditionInfluence.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//===----------------------------------------------------------------------===//
9
11
14#include "llzk/Util/Walk.h"
15
16using namespace mlir;
17using namespace llzk::component;
18using namespace llzk::function;
19using namespace llzk::verif;
20using namespace llzk::verif::detail;
21
22static InfluenceInfo
23makeInfluenceInfo(Influence influence, std::optional<Location> loc = std::nullopt) {
24 InfluenceInfo info;
25 info.influence = influence;
26 if (loc.has_value()) {
27 info.structMemberLocs.insert(loc.value());
28 }
29 return info;
30}
31
32//===------------------------------------------------------------------===//
33// ForbiddenInfluenceAnalyzer::AnalysisFrame
34//===------------------------------------------------------------------===//
35
36ForbiddenInfluenceAnalyzer::AnalysisFrame::AnalysisFrame(
37 ForbiddenInfluenceAnalyzer &parentAnalyzer, CallableOpInterface callableOp,
38 llvm::ArrayRef<InfluenceInfo> argInfluenceInfos, InfluenceInfo inheritedControl
39)
40 : analyzer(parentAnalyzer), inheritedControlInfluence(inheritedControl) {
41 Region *region = callableOp.getCallableRegion();
42 assert(region && !region->empty() && "callable must have a body");
43 Block &entry = region->front();
44 for (auto [arg, influenceInfo] : llvm::zip(entry.getArguments(), argInfluenceInfos)) {
45 valueCache[arg] = influenceInfo;
46 }
47}
48
49InfluenceInfo ForbiddenInfluenceAnalyzer::AnalysisFrame::analyzeValue(Value value) {
50 if (auto it = valueCache.find(value); it != valueCache.end()) {
51 return it->second;
52 }
53 if (!activeValues.insert(value).second) {
54 return makeInfluenceInfo(Influence::None);
55 }
56
57 InfluenceInfo result = makeInfluenceInfo(Influence::None);
58 if (auto blockArg = llvm::dyn_cast<BlockArgument>(value)) {
59 result = analyzeBlockArgument(blockArg);
60 } else if (Operation *defOp = value.getDefiningOp()) {
61 auto opRes = llvm::dyn_cast<OpResult>(value);
62 assert(opRes && "value has defining op, so it must be an op result");
63 if (llvm::isa<MemberReadOp>(defOp)) {
64 result = makeInfluenceInfo(Influence::StructMember, defOp->getLoc());
65 } else if (auto call = llvm::dyn_cast<CallOpInterface>(defOp)) {
66 result = analyzeCallResult(call, opRes);
67 } else if (auto ifOp = llvm::dyn_cast<scf::IfOp>(defOp)) {
68 result = analyzeIfResult(ifOp, opRes);
69 } else if (auto forOp = llvm::dyn_cast<scf::ForOp>(defOp)) {
70 result = analyzeForResult(forOp, opRes);
71 } else if (auto execOp = llvm::dyn_cast<scf::ExecuteRegionOp>(defOp)) {
72 result = analyzeExecuteRegionResult(execOp, opRes);
73 } else if (auto whileOp = llvm::dyn_cast<scf::WhileOp>(defOp)) {
74 result = analyzeWhileResult(whileOp, opRes);
75 } else {
76 for (Value operand : defOp->getOperands()) {
77 result = mergeInfluenceInfo(result, analyzeValue(operand));
78 }
79 }
80 }
81
82 activeValues.erase(value);
83 valueCache[value] = result;
84 return result;
85}
86
87InfluenceInfo ForbiddenInfluenceAnalyzer::AnalysisFrame::analyzePreconditionOp(
88 PreconditionOpInterface preCondOp
89) {
90 return mergeInfluenceInfo(
91 inheritedControlInfluence, analyzeValue(preCondOp.getCondition()),
92 analyzeControlAncestors(preCondOp.getOperation())
93 );
94}
95
96IncludedContractSummary
97ForbiddenInfluenceAnalyzer::AnalysisFrame::analyzeIncludeOp(IncludeOp includeOp) {
98 InfluenceInfo callerControlInfluence = analyzeControlAncestors(includeOp.getOperation());
99 InfluenceInfo calleeControlInfluence =
100 mergeInfluenceInfo(inheritedControlInfluence, callerControlInfluence);
101
102 SymbolTableCollection tables;
103 auto calleeTarget = includeOp.getCalleeTarget(tables);
104 if (failed(calleeTarget)) {
105 IncludedContractSummary summary;
106 summary.failures.push_back(
107 {.preconditionLoc = {},
108 .influenceInfo = mergeInfluenceInfo(
109 makeInfluenceInfo(Influence::FunctionReturn), calleeControlInfluence
110 )}
111 );
112 return summary;
113 }
114
115 ContractOp calleeContract = calleeTarget->get();
116
117 llvm::SmallVector<InfluenceInfo> argInfluences = llvm::map_to_vector(
118 includeOp.getArgOperands(), [this, &calleeControlInfluence](Value operand) {
119 return mergeInfluenceInfo(analyzeValue(operand), calleeControlInfluence);
120 }
121 );
122 return analyzer.analyzeIncludedContract(calleeContract, argInfluences, calleeControlInfluence);
123}
124
125InfluenceInfo ForbiddenInfluenceAnalyzer::AnalysisFrame::analyzeControlAncestors(Operation *op) {
126 if (auto it = controlAncestorCache.find(op); it != controlAncestorCache.end()) {
127 return it->second;
128 }
129
130 InfluenceInfo result = makeInfluenceInfo(Influence::None);
131 Operation *current = op;
132 while (Operation *parentOp = current->getParentOp()) {
133 if (isa<ContractOp>(parentOp)) {
134 break;
135 }
136 result = mergeInfluenceInfo(result, analyzeAncestorControl(parentOp, current));
137 current = parentOp;
138 }
139
140 controlAncestorCache[op] = result;
141 return result;
142}
143
144InfluenceInfo ForbiddenInfluenceAnalyzer::AnalysisFrame::analyzeAncestorControl(
145 Operation *ancestor, Operation *nestedOp
146) {
147 if (auto ifOp = dyn_cast<scf::IfOp>(ancestor)) {
148 return analyzeValue(ifOp.getCondition());
149 }
150 if (auto forOp = dyn_cast<scf::ForOp>(ancestor)) {
151 return mergeInfluenceInfo(
152 analyzeValue(forOp.getLowerBound()), analyzeValue(forOp.getUpperBound()),
153 analyzeValue(forOp.getStep())
154 );
155 }
156 if (auto whileOp = dyn_cast<scf::WhileOp>(ancestor)) {
157 InfluenceInfo result = analyzeValue(whileOp.getConditionOp().getCondition());
158 Region *nestedRegion = nestedOp->getParentRegion();
159 if (nestedRegion == &whileOp.getAfter()) {
160 unsigned beforeArgCount = whileOp.getBefore().front().getNumArguments();
161 for (unsigned i = 0; i < beforeArgCount; ++i) {
162 result =
163 mergeInfluenceInfo(result, analyzeValue(whileOp.getBefore().front().getArgument(i)));
164 }
165 }
166 return result;
167 }
168 return makeInfluenceInfo(Influence::None);
169}
170
172ForbiddenInfluenceAnalyzer::AnalysisFrame::analyzeBlockArgument(BlockArgument blockArg) {
173 Block *owner = blockArg.getOwner();
174
175 Operation *parentOp = owner->getParentOp();
176 if (auto forOp = llvm::dyn_cast<scf::ForOp>(parentOp)) {
177 unsigned argNumber = blockArg.getArgNumber();
178 InfluenceInfo tripCountInfo = mergeInfluenceInfo(
179 analyzeValue(forOp.getLowerBound()), analyzeValue(forOp.getUpperBound()),
180 analyzeValue(forOp.getStep())
181 );
182 // arg0 is the induction var, influenced by the lower bound, upper bound, and
183 // the step operands to the loop.
184 // The other for loop region arguments are additionally influenced by the
185 // init args and yield args. They are also conservatively influenced by the loop trip count
186 // values (bounds and step).
187 if (argNumber != 0) {
188 unsigned iterIndex = argNumber - 1;
189 return mergeInfluenceInfo(
190 tripCountInfo, analyzeValue(forOp.getInitArgs()[iterIndex]),
191 analyzeValue(forOp.getYieldedValues()[iterIndex])
192 );
193 }
194 return tripCountInfo;
195 }
196 if (auto whileOp = llvm::dyn_cast<scf::WhileOp>(parentOp)) {
197 Region *region = owner->getParent();
198 if (region == &whileOp.getBefore()) {
199 // The before-region block arguments are loop-carried, so they depend on
200 // both the initial inputs and the values yielded from the after region.
201 return mergeInfluenceInfo(
202 analyzeValue(whileOp.getInits()[blockArg.getArgNumber()]),
203 analyzeValue(whileOp.getYieldOp().getOperand(blockArg.getArgNumber()))
204 );
205 }
206 if (region == &whileOp.getAfter()) {
207 // The after block is given arguments from the condition op. These arguments
208 // also don't have to align with the before region args, which is confusing at
209 // first glance. https://mlir.llvm.org/docs/Dialects/SCFDialect/#scfwhile-scfwhileop
210 scf::ConditionOp condOp = whileOp.getConditionOp();
211 auto condArgs = condOp.getArgs();
212 assert(
213 condArgs.size() == region->getNumArguments() &&
214 "condition args must equal after region args"
215 );
216 return analyzeValue(condArgs[blockArg.getArgNumber()]);
217 }
218 }
219 return makeInfluenceInfo(Influence::None);
220}
221
222InfluenceInfo ForbiddenInfluenceAnalyzer::AnalysisFrame::analyzeCallResult(
223 CallOpInterface call, OpResult callRes
224) {
225 auto resolvedCallable = llvm::dyn_cast_if_present<CallableOpInterface>(call.resolveCallable());
226 if (!resolvedCallable || !resolvedCallable.getCallableRegion()) {
227 return makeInfluenceInfo(Influence::FunctionReturn);
228 }
229
230 llvm::SmallVector<InfluenceInfo> argInfluences =
231 llvm::map_to_vector(call.getArgOperands(), [this](Value operand) {
232 return analyzeValue(operand);
233 });
234 return analyzer.analyzeCallableResult(resolvedCallable, argInfluences, callRes.getResultNumber());
235}
236
238ForbiddenInfluenceAnalyzer::AnalysisFrame::analyzeIfResult(scf::IfOp ifOp, OpResult ifRes) {
239 unsigned resultNumber = ifRes.getResultNumber();
240 InfluenceInfo result = analyzeValue(ifOp.getCondition());
241 for (scf::YieldOp yieldOp : {ifOp.elseYield(), ifOp.thenYield()}) {
242 if (yieldOp && yieldOp->getNumOperands() > resultNumber) {
243 result = mergeInfluenceInfo(result, analyzeValue(yieldOp->getOperand(resultNumber)));
244 }
245 }
246 return result;
247}
248
250ForbiddenInfluenceAnalyzer::AnalysisFrame::analyzeForResult(scf::ForOp forOp, OpResult forRes) {
251 unsigned resultNumber = forRes.getResultNumber();
252 return mergeInfluenceInfo(
253 analyzeValue(forOp.getLowerBound()), analyzeValue(forOp.getUpperBound()),
254 analyzeValue(forOp.getStep()), analyzeValue(forOp.getInitArgs()[resultNumber]),
255 analyzeValue(forOp.getYieldedValues()[resultNumber])
256 );
257}
258
259InfluenceInfo ForbiddenInfluenceAnalyzer::AnalysisFrame::analyzeExecuteRegionResult(
260 scf::ExecuteRegionOp execOp, OpResult execRes
261) {
262 unsigned resultNumber = execRes.getResultNumber();
263 InfluenceInfo result = makeInfluenceInfo(Influence::None);
264 for (Block &block : execOp.getRegion()) {
265 // Only the terminators of the execute_region's own blocks contribute to
266 // the op result. Nested SCF regions can contain their own scf.yield ops,
267 // but those yields only feed the nested region results and must not be
268 // treated as execute_region return values.
269 if (auto yieldOp = dyn_cast<scf::YieldOp>(block.getTerminator());
270 yieldOp && yieldOp.getNumOperands() > resultNumber) {
271 result = mergeInfluenceInfo(result, analyzeValue(yieldOp.getOperand(resultNumber)));
272 }
273 }
274 return result;
275}
276
277InfluenceInfo ForbiddenInfluenceAnalyzer::AnalysisFrame::analyzeWhileResult(
278 scf::WhileOp whileOp, OpResult whileRes
279) {
280 unsigned resultNumber = whileRes.getResultNumber();
281 // The number of results must match, by definition:
282 // - size of init args
283 // - size of condition op args (excluding the condition value)
284 // - size of yield operands
285 auto inits = whileOp.getInits();
286 scf::ConditionOp condOp = whileOp.getConditionOp();
287 auto condArgs = condOp.getArgs();
288 auto yieldOps = whileOp.getYieldOp().getOperands();
289 assert(
290 inits.size() == condArgs.size() && condArgs.size() == yieldOps.size() &&
291 "invalid while op dimensions"
292 );
293 assert(inits.size() > resultNumber && "invalid result number");
294
295 return mergeInfluenceInfo(
296 analyzeValue(inits[resultNumber]), analyzeValue(condArgs[resultNumber]),
297 analyzeValue(yieldOps[resultNumber]),
298 // The results are also control-flow influenced by the condition value itself.
299 analyzeValue(condOp.getCondition())
300 );
301}
302
303//===------------------------------------------------------------------===//
304// ForbiddenInfluenceAnalyzer
305//===------------------------------------------------------------------===//
306
308 if (auto it = cachedFrames.find(contract); it != cachedFrames.end()) {
309 return it->second.analyzeValue(value);
310 }
311 llvm::SmallVector<InfluenceInfo> argInfluenceInfos =
312 llvm::map_to_vector(contract.getArguments(), [contract](BlockArgument arg) {
313 return classifyContractArgument(contract, arg);
314 });
315 auto [it, inserted] = cachedFrames.try_emplace(contract, *this, contract, argInfluenceInfos);
316 assert(inserted && "lookup failure");
317 return it->second.analyzeValue(value);
318}
319
321 ContractOp contract, PreconditionOpInterface preCondOp
322) {
323 if (auto it = cachedFrames.find(contract); it != cachedFrames.end()) {
324 return it->second.analyzePreconditionOp(preCondOp);
325 }
326
327 llvm::SmallVector<InfluenceInfo> argInfluenceInfos =
328 llvm::map_to_vector(contract.getArguments(), [contract](BlockArgument arg) {
329 return classifyContractArgument(contract, arg);
330 });
331 auto [it, inserted] = cachedFrames.try_emplace(contract, *this, contract, argInfluenceInfos);
332 assert(inserted && "lookup failure");
333 return it->second.analyzePreconditionOp(preCondOp);
334}
335
337 CallableOpInterface callableOp, llvm::ArrayRef<InfluenceInfo> argInfluences,
338 unsigned resultNumber
339) {
341 .callable = callableOp,
342 .argInfluences = llvm::SmallVector<InfluenceInfo>(argInfluences.begin(), argInfluences.end()),
343 .resultNumber = resultNumber,
344 };
345
346 if (auto it = callableSummaryCache.find(key); it != callableSummaryCache.end()) {
347 return it->second;
348 }
349 if (!activeSummaries.insert(key).second) {
350 return makeInfluenceInfo(Influence::FunctionReturn);
351 }
352
353 InfluenceInfo summary = makeInfluenceInfo(Influence::FunctionReturn);
354 Region *region = callableOp.getCallableRegion();
355 if (region && !region->empty()) {
356 AnalysisFrame frame(*this, callableOp, argInfluences);
357 summary = makeInfluenceInfo(Influence::None);
358 region->walk([&](ReturnOp retOp) {
359 if (retOp.getNumOperands() > resultNumber) {
360 summary = mergeInfluenceInfo(summary, frame.analyzeValue(retOp.getOperand(resultNumber)));
361 }
362 });
363 }
364
365 activeSummaries.erase(key);
366 callableSummaryCache[key] = summary;
367 return summary;
368}
369
371 ContractOp calleeContract, llvm::ArrayRef<InfluenceInfo> argInfluences,
372 InfluenceInfo inheritedControlInfluence
373) {
375 .contract = calleeContract,
376 .argInfluences = llvm::SmallVector<InfluenceInfo>(argInfluences.begin(), argInfluences.end()),
377 .inheritedControlInfluence = inheritedControlInfluence,
378 };
379
380 if (auto it = includedContractSummaryCache.find(key); it != includedContractSummaryCache.end()) {
381 return it->second;
382 }
383 if (!activeIncludedSummaries.insert(key).second) {
385 summary.failures.push_back(
386 {.preconditionLoc = {}, .influenceInfo = makeInfluenceInfo(Influence::FunctionReturn)}
387 );
388 return summary;
389 }
390
391 AnalysisFrame frame(*this, calleeContract, argInfluences, inheritedControlInfluence);
393
394 SmallVector<PreconditionOpInterface> preconditionOps =
395 walkCollect<PreconditionOpInterface>(calleeContract);
396 for (PreconditionOpInterface preCondOp : preconditionOps) {
397 InfluenceInfo influenceInfo = frame.analyzePreconditionOp(preCondOp);
398 if (any(influenceInfo.influence)) {
399 summary.failures.push_back(
400 {.preconditionLoc = preCondOp->getLoc(), .influenceInfo = influenceInfo}
401 );
402 }
403 }
404
405 SmallVector<IncludeOp> includeOps = walkCollect<IncludeOp>(calleeContract);
406 for (IncludeOp includeOp : includeOps) {
407 IncludedContractSummary nestedSummary = frame.analyzeIncludeOp(includeOp);
408 summary.failures.append(nestedSummary.failures.begin(), nestedSummary.failures.end());
409 }
410
411 activeIncludedSummaries.erase(key);
412 includedContractSummaryCache[key] = summary;
413 return summary;
414}
415
418 if (auto it = cachedFrames.find(contract); it != cachedFrames.end()) {
419 return it->second.analyzeIncludeOp(includeOp);
420 }
421
422 llvm::SmallVector<InfluenceInfo> argInfluenceInfos =
423 llvm::map_to_vector(contract.getArguments(), [contract](BlockArgument arg) {
424 return classifyContractArgument(contract, arg);
425 });
426 auto [it, inserted] = cachedFrames.try_emplace(contract, *this, contract, argInfluenceInfos);
427 assert(inserted && "lookup failure");
428 return it->second.analyzeIncludeOp(includeOp);
429}
430
432ForbiddenInfluenceAnalyzer::classifyContractArgument(ContractOp contract, BlockArgument arg) {
433 SymbolTableCollection tables;
434 auto funcTarget = contract.getFuncTarget(tables);
435 if (failed(funcTarget)) {
436 return makeInfluenceInfo(Influence::None);
437 }
438
439 unsigned numFuncInputs = funcTarget->get().getFunctionType().getNumInputs();
440 if (arg.getArgNumber() >= numFuncInputs) {
441 return makeInfluenceInfo(Influence::FunctionReturn);
442 }
443 return makeInfluenceInfo(Influence::None);
444}
This file contains an analysis and utilities for determining if a verif precondition is dependent,...
::mlir::FailureOr< SymbolLookupResult< function::FuncDefOp > > getFuncTarget(::mlir::SymbolTableCollection &tables)
Return the FuncDefOp that this contract targets, or failure if it does not target a function or the f...
::mlir::Operation::operand_range getArgOperands()
Definition Ops.h.inc:1075
::mlir::FailureOr<::llzk::SymbolLookupResult<::llzk::verif::ContractOp > > getCalleeTarget(::mlir::SymbolTableCollection &tables)
Resolve and return the target Contract for this CallOp.
Definition Ops.cpp:1023
::mlir::TypedValue<::mlir::IntegerType > getCondition()
Gets the SSA Value for the condition operand.
Interprocedural verifier-local analysis for forbidden precondition influence.
InfluenceInfo analyzeCallableResult(mlir::CallableOpInterface callableOp, llvm::ArrayRef< InfluenceInfo > argInfluences, unsigned resultNumber)
Summarize the forbidden influence of one callable result under the given argument influences.
IncludedContractSummary analyzeIncludedOp(verif::ContractOp contract, verif::IncludeOp includeOp)
Check whether an include op becomes invalid under its caller's operand bindings and enclosing SCF con...
InfluenceInfo analyzePreconditionOp(verif::ContractOp contract, verif::PreconditionOpInterface preCondOp)
Classify the forbidden influence reaching a precondition op, including both the condition operand and...
InfluenceInfo analyzeContractValue(verif::ContractOp contract, mlir::Value value)
Classify the forbidden influence reaching a value inside a contract body.
IncludedContractSummary analyzeIncludedContract(verif::ContractOp calleeContract, llvm::ArrayRef< InfluenceInfo > argInfluences, InfluenceInfo inheritedControlInfluence=InfluenceInfo::None())
Check whether an included contract becomes invalid under caller-provided operand influences,...
ForbiddenPreconditionInfluence Influence
ForbiddenPreconditionInfluenceInfo InfluenceInfo
ForbiddenPreconditionInfluenceInfo mergeInfluenceInfo(ForbiddenPreconditionInfluenceInfo lhs, const ForbiddenPreconditionInfluenceInfo &rhs)
Merge two forbidden-influence summaries, preserving the first known source location for each forbidde...
bool any(ForbiddenPreconditionInfluence influence)
Return true when the influence set contains at least one classification.
llvm::SmallSetVector< mlir::Location, 2 > structMemberLocs
Cache key for one interprocedural callable-result summary query.
Cache key for one interprocedural included-contract summary query.
Summary of all included-contract precondition failures under a specific caller binding.
llvm::SmallVector< IncludedContractFailure > failures