LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
IntervalAnalysis.cpp
Go to the documentation of this file.
1//===-- IntervalAnalysis.cpp - Interval analysis implementation -*- 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//===----------------------------------------------------------------------===//
9
11
17#include "llzk/Util/Debug.h"
20
21#include <mlir/Analysis/DataFlow/DeadCodeAnalysis.h>
22#include <mlir/Dialect/SCF/IR/SCF.h>
23
24#include <llvm/ADT/EquivalenceClasses.h>
25#include <llvm/ADT/TypeSwitch.h>
26
27#include <functional>
28
29using namespace mlir;
30
31namespace llzk {
32
33using namespace array;
34using namespace boolean;
35using namespace cast;
36using namespace component;
37using namespace constrain;
38using namespace felt;
39using namespace function;
40
41namespace {
42
43std::optional<UnreducedInterval> mergeUnreducedIntervals(
44 const std::optional<UnreducedInterval> &lhs, const std::optional<UnreducedInterval> &rhs
45) {
46 if (!lhs.has_value() || !rhs.has_value()) {
47 return std::nullopt;
48 }
49 return lhs->doUnion(*rhs);
50}
51
52template <typename Fn>
53std::optional<UnreducedInterval>
54combineUnreducedIntervals(const ExpressionValue &lhs, const ExpressionValue &rhs, Fn &&fn) {
55 if (!lhs.hasUnreducedInterval() || !rhs.hasUnreducedInterval()) {
56 return std::nullopt;
57 }
58 return fn(lhs.getUnreducedInterval(), rhs.getUnreducedInterval());
59}
60
61ExpressionValue refineReducedInterval(const ExpressionValue &expr, const Interval &newInterval) {
62 ExpressionValue refined = expr.withInterval(newInterval);
63 if (expr.getInterval() != newInterval) {
64 refined = refined.dropUnreducedInterval();
65 }
66 return refined;
67}
68
69bool isInMaybeSkippedScfRegion(Operation *op) {
70 for (Operation *parent = op->getParentOp(); parent != nullptr; parent = parent->getParentOp()) {
71 if (llvm::isa<FuncDefOp>(parent)) {
72 return false;
73 }
74
75 // `writeResults` is a storage side-channel, not a path-sensitive lattice.
76 // Writes nested under branch/loop control may not be absolute on every path through the
77 // enclosing op, so keep the prior state instead of treating the nested write as unconditional.
78 if (llvm::isa<scf::ForOp, scf::IfOp, scf::WhileOp>(parent)) {
79 return true;
80 }
81 }
82 return false;
83}
84
85std::optional<UnreducedInterval> getBooleanUnreducedInterval(const Interval &interval) {
86 return interval.isBoolean() ? std::optional<UnreducedInterval>(interval.firstUnreduced())
87 : std::nullopt;
88}
89
90FailureOr<std::vector<SourceRef>>
91translateRef(const SourceRef &ref, const SourceRefRemappings &translations) {
92 std::vector<SourceRef> refs;
93 for (const auto &[prefix, vals] : translations) {
94 if (!ref.isValidPrefix(prefix)) {
95 continue;
96 }
97
98 if (vals.isArray()) {
99 auto suffix = ref.getSuffix(prefix);
100 ensure(succeeded(suffix), "prefix checked before SourceRef suffix extraction");
101
102 std::vector<SourceRefIndex> arraySuffix, remainingSuffix;
103 bool suffixIsPastArray = false;
104 for (const SourceRefIndex &idx : *suffix) {
105 if (!suffixIsPastArray && arraySuffix.size() < vals.getNumArrayDims() &&
106 (idx.isIndex() || idx.isIndexRange())) {
107 arraySuffix.push_back(idx);
108 continue;
109 }
110 suffixIsPastArray = true;
111 remainingSuffix.push_back(idx);
112 }
113
114 auto resolvedValsRes = vals.extract(arraySuffix);
115 ensure(succeeded(resolvedValsRes), "could not resolve translated SourceRef array child");
116 SourceRefSet folded = resolvedValsRes->first.foldToScalar();
117 if (remainingSuffix.empty()) {
118 refs.insert(refs.end(), folded.begin(), folded.end());
119 continue;
120 }
121
122 for (const SourceRef &baseRef : folded) {
123 auto translatedRef = mlir::FailureOr<SourceRef>(baseRef);
124 for (const SourceRefIndex &idx : remainingSuffix) {
125 if (failed(translatedRef)) {
126 break;
127 }
128 translatedRef = translatedRef->createChild(idx);
129 }
130 if (succeeded(translatedRef)) {
131 refs.push_back(*translatedRef);
132 }
133 }
134 } else {
135 for (const SourceRef &replacement : vals.getScalarValue()) {
136 auto translated = ref.translate(prefix, replacement);
137 if (succeeded(translated)) {
138 refs.push_back(*translated);
139 }
140 }
141 }
142 }
143
144 if (refs.empty()) {
145 return failure();
146 }
147 return refs;
148}
149
150bool isDirectSourceRefValue(Value value) {
151 if (llvm::isa<BlockArgument>(value)) {
152 return true;
153 }
154
155 Operation *definingOp = value.getDefiningOp();
156 return llvm::isa_and_present<MemberReadOp, ReadArrayOp, polymorphic::ConstReadOp>(definingOp);
157}
158
159std::optional<SourceRefLatticeValue>
160getIdentitySourceRefState(DataFlowSolver &solver, Value value) {
161 if (isDirectSourceRefValue(value)) {
163 if (val.isScalar()) {
164 return val;
165 }
166 return std::nullopt;
167 }
168
169 auto createArray = llvm::dyn_cast_if_present<CreateArrayOp>(value.getDefiningOp());
170 if (!createArray) {
171 return std::nullopt;
172 }
173
174 SourceRefLatticeValue arrayVal(createArray.getType().getShape());
175 for (auto [idx, element] : llvm::enumerate(createArray.getElements())) {
176 std::optional<SourceRefLatticeValue> elementVal = getIdentitySourceRefState(solver, element);
177 if (!elementVal.has_value()) {
178 return std::nullopt;
179 }
180 (void)arrayVal.getElemFlatIdx(idx).setValue(*elementVal);
181 }
182 return arrayVal;
183}
184
185llvm::EquivalenceClasses<SourceRef>
186collectDirectEqualityRefs(DataFlowSolver &solver, FuncDefOp fn) {
187 llvm::EquivalenceClasses<SourceRef> eqRefs;
188 fn.walk([&](EmitEqualityOp eqOp) {
189 Operation *op = eqOp.getOperation();
190 if (!dataflow::isOperationLive(solver, op)) {
191 return;
192 }
193
194 Value lhs = eqOp.getLhs();
195 Value rhs = eqOp.getRhs();
196 if (!isDirectSourceRefValue(lhs) || !isDirectSourceRefValue(rhs)) {
197 return;
198 }
199
202 if (!lhsState.isScalar() || !rhsState.isScalar() || !lhsState.isSingleValue() ||
203 !rhsState.isSingleValue()) {
204 return;
205 }
206
207 const SourceRef &lhsRef = lhsState.getSingleValue();
208 const SourceRef &rhsRef = rhsState.getSingleValue();
209 if (lhsRef.isConstant() || rhsRef.isConstant()) {
210 return;
211 }
212 eqRefs.unionSets(lhsRef, rhsRef);
213 });
214 return eqRefs;
215}
216
217} // namespace
218
219/* ExpressionValue */
220
221llvm::SMTExprRef createFieldInverseExpr(
222 const llvm::SMTSolverRef &solver, Operation *op, const ExpressionValue &val,
223 StringRef suffix = ""
224) {
225 const Field &field = val.getField();
226 const Interval &iv = val.getInterval();
227 if (iv.isDegenerate() && iv.lhs() != field.zero()) {
228 DynamicAPInt invVal = field.inv(iv.lhs());
229 return solver->mkBitvector(toAPSInt(invVal), field.bitWidth());
230 }
231
232 // The definition of an inverse X^-1 is Y s.t. XY % prime = 1.
233 // To create this expression, we create a new symbol for Y and add the
234 // XY % prime = 1 constraint to the solver.
235 std::string symName = buildStringViaInsertionOp(*op);
236 if (!suffix.empty()) {
237 symName += suffix.str();
238 }
239 llvm::SMTExprRef invSym = field.createSymbol(solver, symName.c_str());
240 llvm::SMTExprRef one = solver->mkBitvector(APSInt::get(1), field.bitWidth());
241 llvm::SMTExprRef prime = solver->mkBitvector(toAPSInt(field.prime()), field.bitWidth());
242 llvm::SMTExprRef mult = solver->mkBVMul(val.getExpr(), invSym);
243 llvm::SMTExprRef mod = solver->mkBVURem(mult, prime);
244 llvm::SMTExprRef constraint = solver->mkEqual(mod, one);
245 solver->addConstraint(constraint);
246 return invSym;
247}
248
250 if (expr == nullptr && rhs.expr == nullptr) {
251 return i == rhs.i && unreduced == rhs.unreduced;
252 }
253 if (expr == nullptr || rhs.expr == nullptr) {
254 return false;
255 }
256 return i == rhs.i && unreduced == rhs.unreduced && *expr == *rhs.expr;
257}
258
260boolToFelt(const llvm::SMTSolverRef &solver, const ExpressionValue &expr, unsigned bitwidth) {
261 llvm::SMTExprRef zero = solver->mkBitvector(mlir::APSInt::get(0), bitwidth);
262 llvm::SMTExprRef one = solver->mkBitvector(mlir::APSInt::get(1), bitwidth);
263 llvm::SMTExprRef boolToFeltConv = solver->mkIte(expr.getExpr(), one, zero);
264 return expr.withExpression(boolToFeltConv);
265}
266
268 const llvm::SMTSolverRef &solver, const ExpressionValue &cond, const ExpressionValue &trueVal,
269 const ExpressionValue &falseVal
270) {
271 const Field &f = trueVal.getField();
272 const Interval &condInterval = cond.getInterval();
273 Interval resultInterval;
274 if (condInterval.isEmpty()) {
275 resultInterval = Interval::Empty(f);
276 } else if (condInterval.isDegenerate() && condInterval.rhs() == f.one()) {
277 resultInterval = trueVal.getInterval();
278 } else if (condInterval.isDegenerate() && condInterval.rhs() == f.zero()) {
279 resultInterval = falseVal.getInterval();
280 } else {
281 resultInterval = trueVal.getInterval().join(falseVal.getInterval());
282 }
283 llvm::SMTExprRef resultExpr =
284 solver->mkIte(cond.getExpr(), trueVal.getExpr(), falseVal.getExpr());
285 std::optional<UnreducedInterval> resultUnreduced;
286 if (condInterval.isEmpty()) {
287 resultUnreduced = resultInterval.firstUnreduced();
288 } else if (condInterval.isDegenerate() && condInterval.rhs() == f.one()) {
289 resultUnreduced = trueVal.getOptionalUnreducedInterval();
290 } else if (condInterval.isDegenerate() && condInterval.rhs() == f.zero()) {
291 resultUnreduced = falseVal.getOptionalUnreducedInterval();
292 } else {
293 resultUnreduced = mergeUnreducedIntervals(
295 );
296 }
297 return ExpressionValue(resultExpr, resultInterval, std::move(resultUnreduced));
298}
299
301 const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs
302) {
303 Interval res = lhs.i.intersect(rhs.i);
304 const auto *exprEq = solver->mkEqual(lhs.expr, rhs.expr);
305 return ExpressionValue(exprEq, res);
306}
307
309add(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs) {
310 ExpressionValue res;
311 res.i = lhs.i + rhs.i;
312 res.expr = solver->mkBVAdd(lhs.expr, rhs.expr);
313 res = res.withOptionalUnreducedInterval(combineUnreducedIntervals(lhs, rhs, std::plus {}));
314 return res;
315}
316
318sub(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs) {
319 ExpressionValue res;
320 res.i = lhs.i - rhs.i;
321 res.expr = solver->mkBVSub(lhs.expr, rhs.expr);
322 res = res.withOptionalUnreducedInterval(combineUnreducedIntervals(lhs, rhs, std::minus {}));
323 return res;
324}
325
327mul(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs) {
328 ExpressionValue res;
329 res.i = lhs.i * rhs.i;
330 res.expr = solver->mkBVMul(lhs.expr, rhs.expr);
331 res = res.withOptionalUnreducedInterval(combineUnreducedIntervals(lhs, rhs, std::multiplies {}));
332 return res;
333}
334
336div(const llvm::SMTSolverRef &solver, Operation *op, const ExpressionValue &lhs,
337 const ExpressionValue &rhs) {
338 ExpressionValue res;
339 auto divRes = feltDiv(lhs.i, rhs.i);
340 if (failed(divRes)) {
341 const Field &field = lhs.getField();
342 const Interval &rhsInterval = rhs.getInterval();
343 Interval zero = Interval::Degenerate(field, field.zero());
344 if (!rhsInterval.isDegenerate()) {
345 if (rhsInterval.intersect(zero).isNotEmpty()) {
346 op->emitWarning(
347 "non-degenerate felt.div divisors are not tracked precisely, and the divisor may "
348 "contain zero. Range of division result will be treated as unbounded."
349 )
350 .report();
351 } else {
352 op->emitWarning(
353 "non-degenerate felt.div divisors are not tracked precisely because precise field "
354 "division over intervals would require enumerating divisor inverses. Range of "
355 "division result will be treated as unbounded."
356 )
357 .report();
358 }
359 } else {
360 op->emitWarning(
361 "divisor is zero, leading to a divide-by-zero error. Range of division result will "
362 "be treated as unbounded."
363 )
364 .report();
365 }
366 res.i = Interval::Entire(lhs.getField());
367 } else {
368 res.i = *divRes;
369 }
370 llvm::SMTExprRef invExpr = createFieldInverseExpr(solver, op, rhs, ".div_inv");
371 res.expr = solver->mkBVMul(lhs.expr, invExpr);
372 return res;
373}
374
376 const llvm::SMTSolverRef &solver, Operation *op, const ExpressionValue &lhs,
377 const ExpressionValue &rhs
378) {
379 ExpressionValue res;
380 auto divRes = unsignedIntDiv(lhs.i, rhs.i);
381 if (failed(divRes)) {
382 op->emitWarning(
383 "divisor is not restricted to non-zero values, leading to potential divide-by-zero error."
384 " Range of division result will be treated as unbounded."
385 )
386 .report();
387 res.i = Interval::Entire(lhs.getField());
388 } else {
389 res.i = *divRes;
390 }
391 res.expr = solver->mkBVUDiv(lhs.expr, rhs.expr);
392 return res;
393}
394
396 const llvm::SMTSolverRef &solver, Operation *op, const ExpressionValue &lhs,
397 const ExpressionValue &rhs
398) {
399 ExpressionValue res;
400 auto divRes = signedIntDiv(lhs.i, rhs.i);
401 if (failed(divRes)) {
402 op->emitWarning(
403 "divisor is not restricted to non-zero values, leading to potential divide-by-zero error."
404 " Range of division result will be treated as unbounded."
405 )
406 .report();
407 res.i = Interval::Entire(lhs.getField());
408 } else {
409 res.i = *divRes;
410 }
411 res.expr = solver->mkBVSDiv(lhs.expr, rhs.expr);
412 return res;
413}
414
415ExpressionValue
416mod(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs) {
417 ExpressionValue res;
418 res.i = lhs.i % rhs.i;
419 res.expr = solver->mkBVURem(lhs.expr, rhs.expr);
420 return res;
421}
422
424sintMod(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs) {
425 return ExpressionValue(
426 solver->mkBVSRem(lhs.getExpr(), rhs.getExpr()),
427 signedMod(lhs.getInterval(), rhs.getInterval())
428 );
429}
430
431ExpressionValue
432bitAnd(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs) {
433 ExpressionValue res;
434 res.i = lhs.i & rhs.i;
435 res.expr = solver->mkBVAnd(lhs.expr, rhs.expr);
436 return res;
437}
438
440bitOr(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs) {
441 ExpressionValue res;
442 res.i = lhs.i | rhs.i;
443 res.expr = solver->mkBVOr(lhs.expr, rhs.expr);
444 return res;
445}
446
448bitXor(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs) {
449 if (lhs.isBoolSort(solver) && rhs.isBoolSort(solver)) {
450 return boolXor(solver, lhs, rhs);
451 }
452
453 ExpressionValue res;
454 res.i = lhs.i ^ rhs.i;
455 res.expr = solver->mkBVXor(lhs.expr, rhs.expr);
456 return res;
457}
458
460 const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs
461) {
462 ExpressionValue res;
463 res.i = lhs.i << rhs.i;
464 res.expr = solver->mkBVShl(lhs.expr, rhs.expr);
465 return res;
466}
467
469 const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs
470) {
471 ExpressionValue res;
472 res.i = lhs.i >> rhs.i;
473 res.expr = solver->mkBVLshr(lhs.expr, rhs.expr);
474 return res;
475}
476
478cmp(const llvm::SMTSolverRef &solver, CmpOp op, const ExpressionValue &lhs,
479 const ExpressionValue &rhs) {
480 ExpressionValue res;
481 const Field &f = lhs.getField();
482 // Default result is any boolean output for when we are unsure about the comparison result.
483 res.i = Interval::Boolean(f);
484 switch (op.getPredicate()) {
485 case FeltCmpPredicate::EQ:
486 res.expr = solver->mkEqual(lhs.expr, rhs.expr);
487 if (lhs.i.isDegenerate() && rhs.i.isDegenerate()) {
488 res.i = lhs.i == rhs.i ? Interval::True(f) : Interval::False(f);
489 } else if (lhs.i.intersect(rhs.i).isEmpty()) {
490 res.i = Interval::False(f);
491 }
492 break;
493 case FeltCmpPredicate::NE:
494 res.expr = solver->mkNot(solver->mkEqual(lhs.expr, rhs.expr));
495 if (lhs.i.isDegenerate() && rhs.i.isDegenerate()) {
496 res.i = lhs.i != rhs.i ? Interval::True(f) : Interval::False(f);
497 } else if (lhs.i.intersect(rhs.i).isEmpty()) {
498 res.i = Interval::True(f);
499 }
500 break;
501 case FeltCmpPredicate::LT:
502 res.expr = solver->mkBVUlt(lhs.expr, rhs.expr);
503 if (lhs.i.toUnreduced().computeGEPart(rhs.i.toUnreduced()).reduce(f).isEmpty()) {
504 res.i = Interval::True(f);
505 }
506 if (lhs.i.toUnreduced().computeLTPart(rhs.i.toUnreduced()).reduce(f).isEmpty()) {
507 res.i = Interval::False(f);
508 }
509 break;
510 case FeltCmpPredicate::LE:
511 res.expr = solver->mkBVUle(lhs.expr, rhs.expr);
512 if (lhs.i.toUnreduced().computeGTPart(rhs.i.toUnreduced()).reduce(f).isEmpty()) {
513 res.i = Interval::True(f);
514 }
515 if (lhs.i.toUnreduced().computeLEPart(rhs.i.toUnreduced()).reduce(f).isEmpty()) {
516 res.i = Interval::False(f);
517 }
518 break;
519 case FeltCmpPredicate::GT:
520 res.expr = solver->mkBVUgt(lhs.expr, rhs.expr);
521 if (lhs.i.toUnreduced().computeLEPart(rhs.i.toUnreduced()).reduce(f).isEmpty()) {
522 res.i = Interval::True(f);
523 }
524 if (lhs.i.toUnreduced().computeGTPart(rhs.i.toUnreduced()).reduce(f).isEmpty()) {
525 res.i = Interval::False(f);
526 }
527 break;
528 case FeltCmpPredicate::GE:
529 res.expr = solver->mkBVUge(lhs.expr, rhs.expr);
530 if (lhs.i.toUnreduced().computeLTPart(rhs.i.toUnreduced()).reduce(f).isEmpty()) {
531 res.i = Interval::True(f);
532 }
533 if (lhs.i.toUnreduced().computeGEPart(rhs.i.toUnreduced()).reduce(f).isEmpty()) {
534 res.i = Interval::False(f);
535 }
536 break;
537 }
538 res = res.withOptionalUnreducedInterval(getBooleanUnreducedInterval(res.i));
539 return res;
540}
541
543boolAnd(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs) {
544 ExpressionValue res;
545 res.i = boolAnd(lhs.i, rhs.i);
546 res.expr = solver->mkAnd(lhs.expr, rhs.expr);
547 res = res.withOptionalUnreducedInterval(getBooleanUnreducedInterval(res.i));
548 return res;
549}
550
552boolOr(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs) {
553 ExpressionValue res;
554 res.i = boolOr(lhs.i, rhs.i);
555 res.expr = solver->mkOr(lhs.expr, rhs.expr);
556 res = res.withOptionalUnreducedInterval(getBooleanUnreducedInterval(res.i));
557 return res;
558}
559
561boolXor(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs) {
562 ExpressionValue res;
563 res.i = boolXor(lhs.i, rhs.i);
564 // There's no Xor, so we do (L || R) && !(L && R)
565 res.expr = solver->mkAnd(
566 solver->mkOr(lhs.expr, rhs.expr), solver->mkNot(solver->mkAnd(lhs.expr, rhs.expr))
567 );
568 res = res.withOptionalUnreducedInterval(getBooleanUnreducedInterval(res.i));
569 return res;
570}
571
572ExpressionValue neg(const llvm::SMTSolverRef &solver, const ExpressionValue &val) {
573 ExpressionValue res;
574 res.i = -val.i;
575 res.expr = solver->mkBVNeg(val.expr);
576 if (val.hasUnreducedInterval()) {
578 }
579 return res;
580}
581
582ExpressionValue notOp(const llvm::SMTSolverRef &solver, const ExpressionValue &val) {
583 ExpressionValue res;
584 res.i = ~val.i;
585 res.expr = solver->mkBVNot(val.expr);
586 return res;
587}
588
589ExpressionValue boolNot(const llvm::SMTSolverRef &solver, const ExpressionValue &val) {
590 ExpressionValue res;
591 res.i = boolNot(val.i);
592 res.expr = solver->mkNot(val.expr);
593 res = res.withOptionalUnreducedInterval(getBooleanUnreducedInterval(res.i));
594 return res;
595}
596
598fallbackUnaryOp(const llvm::SMTSolverRef &solver, Operation *op, const ExpressionValue &val) {
599 const Field &field = val.getField();
600 ExpressionValue res;
601 res.i = Interval::Entire(field);
602 res.expr = TypeSwitch<Operation *, llvm::SMTExprRef>(op)
603 .Case<InvFeltOp>([&](auto) {
604 return createFieldInverseExpr(solver, op, val);
605 }).Default([](Operation *unsupported) {
606 llvm::report_fatal_error(
607 "no fallback provided for " + mlir::Twine(unsupported->getName().getStringRef())
608 );
609 return nullptr;
610 });
611
612 if (llvm::isa<InvFeltOp>(op)) {
613 // We have the inverse's unreduced range to be [0, p-1] because for any integer z we can always
614 // choose a conical element x \in [0, p-1] such that 1) (z * x) %p = 0 if z = 0, 2) (z * x) % p
615 // = 1
616 res = res.withOptionalUnreducedInterval(UnreducedInterval(field.zero(), field.maxVal()));
617 }
618
619 return res;
620}
621
622void ExpressionValue::print(mlir::raw_ostream &os) const {
623 if (expr) {
624 expr->print(os);
625 } else {
626 os << "<null expression>";
627 }
628
629 os << " ( interval: " << i << " )";
630 if (unreduced.has_value()) {
631 os << " ( unreduced: " << *unreduced << " )";
632 }
633}
634
635/* IntervalAnalysisLattice */
636
637ChangeResult IntervalAnalysisLattice::join(const AbstractSparseLattice & /*other*/) {
638 // The update logic is handled in visitOperation; we don't support a generic
639 // join operation, as it may override valid intervals.
640 return ChangeResult::NoChange;
641}
642
643ChangeResult IntervalAnalysisLattice::meet(const AbstractSparseLattice & /*other*/) {
644 // The update logic is handled in visitOperation; we don't support a generic
645 // meet operation, as it may override valid intervals.
646 return ChangeResult::NoChange;
647}
648
649void IntervalAnalysisLattice::print(mlir::raw_ostream &os) const {
650 os << "IntervalAnalysisLattice { " << val << " }";
651}
652
654 if (val == newVal) {
655 return ChangeResult::NoChange;
656 }
657 val = newVal;
658 return ChangeResult::Change;
659}
660
662 LatticeValue newVal(e);
663 return setValue(newVal);
664}
665
667 if (!constraints.contains(e)) {
668 constraints.insert(e);
669 return ChangeResult::Change;
670 }
671 return ChangeResult::NoChange;
672}
673
674/* IntervalDataFlowAnalysis */
675
676SourceRefLatticeValue IntervalDataFlowAnalysis::getSourceRefState(Value val) {
677 return SourceRefAnalysis::getValueState(_dataflowSolver, val);
678}
679
680std::vector<SourceRefIndex> IntervalDataFlowAnalysis::getArrayAccessIndices(
681 Operation * /*baseOp*/, ArrayAccessOpInterface arrayAccessOp
682) {
683 std::vector<SourceRefIndex> indices;
684 ArrayType arrayType = arrayAccessOp.getArrRefType();
685 size_t numIndices = arrayAccessOp.getIndices().size();
686 indices.reserve(numIndices);
687
688 for (size_t i = 0; i < numIndices; ++i) {
689 Value idxOperand = arrayAccessOp.getIndices()[i];
690 SourceRefLatticeValue idxVals = getSourceRefState(idxOperand);
691
692 // Only exact constant indices get tracked precisely.
693 if (idxVals.isSingleValue() && idxVals.getSingleValue().isConstant()) {
694 indices.emplace_back(*idxVals.getSingleValue().getConstantValue());
695 } else {
696 auto lower = APInt::getZero(64);
697 APInt upper(64, arrayType.getDimSize(i));
698 indices.emplace_back(lower, upper);
699 }
700 }
701
702 return indices;
703}
704
705mlir::FailureOr<SourceRef> IntervalDataFlowAnalysis::getArrayAccessRef(
706 Operation *baseOp, ArrayAccessOpInterface arrayAccessOp
707) {
708 std::vector<SourceRefIndex> indices = getArrayAccessIndices(baseOp, arrayAccessOp);
709 Value arrayVal = arrayAccessOp.getArrRef();
710 if (auto blockArg = llvm::dyn_cast<BlockArgument>(arrayVal)) {
711 return SourceRef(blockArg, std::move(indices));
712 }
713 if (auto result = llvm::dyn_cast<OpResult>(arrayVal)) {
714 return SourceRef(result, std::move(indices));
715 }
716 return failure();
717}
718
719Interval IntervalDataFlowAnalysis::getRefInterval(const SourceRef &ref) {
720 if (auto it = writeResults.find(ref); it != writeResults.end()) {
721 return it->second.getInterval();
722 }
723
724 if (ref.isConstantInt()) {
725 auto constVal = ref.getConstantValue();
726 if (succeeded(constVal)) {
727 return Interval::Degenerate(field.get(), *constVal);
728 }
729 }
730
731 if (ref.isRooted() && ref.getPath().empty()) {
732 auto rootVal = ref.getRoot();
733 if (succeeded(rootVal) && !llvm::isa<ArrayType, StructType, pod::PodType>(rootVal->getType())) {
734 const ExpressionValue &rootExpr = getLatticeElement(*rootVal)->getValue().getScalarValue();
735 if (rootExpr.getExpr() != nullptr) {
736 return rootExpr.getInterval();
737 }
738 }
739 }
740
741 return getDefaultIntervalForType(ref.getType());
742}
743
744std::optional<UnreducedInterval>
745IntervalDataFlowAnalysis::getDefaultUnreducedIntervalForType(mlir::Type ty) const {
746 if (!trackUnreducedIntervals) {
747 return std::nullopt;
748 }
749 if (isBooleanType(ty)) {
750 return UnreducedInterval(0, 1);
751 }
752 return UnreducedInterval(field.get().zero(), field.get().maxVal());
753}
754
755std::optional<UnreducedInterval>
756IntervalDataFlowAnalysis::getRefUnreducedInterval(const SourceRef &ref) {
757 if (!trackUnreducedIntervals) {
758 return std::nullopt;
759 }
760
761 if (auto it = writeResults.find(ref); it != writeResults.end()) {
762 return it->second.getOptionalUnreducedInterval();
763 }
764
765 if (ref.isConstantInt()) {
766 auto constVal = ref.getConstantValue();
767 if (succeeded(constVal)) {
768 return UnreducedInterval(*constVal, *constVal);
769 }
770 }
771
772 if (ref.isRooted() && ref.getPath().empty()) {
773 auto rootVal = ref.getRoot();
774 if (succeeded(rootVal) && !llvm::isa<ArrayType, StructType, pod::PodType>(rootVal->getType())) {
775 const ExpressionValue &rootExpr = getLatticeElement(*rootVal)->getValue().getScalarValue();
776 if (rootExpr.hasUnreducedInterval()) {
777 return rootExpr.getUnreducedInterval();
778 }
779 }
780 }
781
782 return getRefInterval(ref).firstUnreduced();
783}
784
785ExpressionValue IntervalDataFlowAnalysis::getRefValue(const SourceRef &ref, Value val) {
786 if (auto it = writeResults.find(ref); it != writeResults.end()) {
787 return it->second;
788 }
789 return createUnknownValue(val)
790 .withInterval(getRefInterval(ref))
791 .withOptionalUnreducedInterval(getRefUnreducedInterval(ref));
792}
793
794void IntervalDataFlowAnalysis::recordRefWrite(
795 const SourceRef &writtenRef, const ExpressionValue &writeVal, bool mayBeSkipped
796) {
797 auto joinStoredWrite = [this, &writtenRef](
798 const ExpressionValue &old, const ExpressionValue &next
799 ) -> ExpressionValue {
800 Interval combinedWrite = old.getInterval().join(next.getInterval());
801 auto combinedUnreduced = mergeUnreducedIntervals(
802 old.getOptionalUnreducedInterval(), next.getOptionalUnreducedInterval()
803 );
804 if (old.getExpr() != nullptr && next.getExpr() != nullptr &&
805 *old.getExpr() == *next.getExpr()) {
806 return old.withInterval(combinedWrite).withOptionalUnreducedInterval(combinedUnreduced);
807 }
808
809 return ExpressionValue(
810 getOrCreateSymbol(writtenRef), combinedWrite, std::move(combinedUnreduced)
811 );
812 };
813
814 if (auto it = writeResults.find(writtenRef); it != writeResults.end()) {
815 it->second = joinStoredWrite(it->second, writeVal);
816 } else if (mayBeSkipped) {
817 ExpressionValue noWrite(
818 getOrCreateSymbol(writtenRef), getRefInterval(writtenRef),
819 getRefUnreducedInterval(writtenRef)
820 );
821 writeResults[writtenRef] = joinStoredWrite(noWrite, writeVal);
822 } else {
823 writeResults[writtenRef] = writeVal;
824 }
825
826 const ExpressionValue &readerUpdate = mayBeSkipped ? writeResults[writtenRef] : writeVal;
827 for (Lattice *readerLattice : readResults[writtenRef]) {
828 ExpressionValue prior = readerLattice->getValue().getScalarValue();
829 Interval intersection = prior.getInterval().intersect(readerUpdate.getInterval());
830 ExpressionValue newVal = prior.withInterval(intersection);
831 propagateIfChanged(readerLattice, readerLattice->setValue(newVal));
832 }
833}
834
836 Operation *op, ArrayRef<const Lattice *> operands, ArrayRef<Lattice *> results
837) {
838 // We only perform the visitation on operations within functions
839 FuncDefOp fn = op->getParentOfType<FuncDefOp>();
840 if (!fn) {
841 return success();
842 }
843
844 // If there are no operands or results, skip.
845 if (operands.empty() && results.empty()) {
846 return success();
847 }
848
849 // Get the values or defaults from the operand lattices
850 llvm::SmallVector<LatticeValue> operandVals;
851 llvm::SmallVector<std::optional<SourceRef>> operandRefs;
852 auto resolveRefStateValue =
853 [&](Value value, const SourceRefLatticeValue &refSet) -> std::optional<LatticeValue> {
854 ensure(refSet.isScalar(), "should have ruled out array values already");
855
856 if (refSet.getScalarValue().empty()) {
857 // If we can't compute the reference, then there must be some unsupported
858 // op the reference analysis cannot handle. We emit a warning and return
859 // early, since there's no meaningful computation we can do for this op.
860 op->emitWarning()
861 .append(
862 "state of ", value,
863 " is empty; defining operation is unsupported by SourceRef analysis"
864 )
865 .report();
866 return std::nullopt;
867 }
868
869 if (!refSet.isSingleValue()) {
870 Interval joinedInterval = Interval::Empty(field.get());
871 std::optional<UnreducedInterval> joinedUnreduced = std::nullopt;
872 bool sawFirst = false;
873 for (const SourceRef &ref : refSet.getScalarValue()) {
874 joinedInterval = joinedInterval.join(getRefInterval(ref));
875 auto refUnreduced = getRefUnreducedInterval(ref);
876 if (!sawFirst) {
877 joinedUnreduced = refUnreduced;
878 sawFirst = true;
879 } else {
880 joinedUnreduced = mergeUnreducedIntervals(joinedUnreduced, refUnreduced);
881 }
882 }
883 ExpressionValue anyVal = createUnknownValue(value)
884 .withInterval(joinedInterval)
885 .withOptionalUnreducedInterval(joinedUnreduced);
886 return LatticeValue(anyVal);
887 }
888
889 return LatticeValue(getRefValue(refSet.getSingleValue(), value));
890 };
891 for (unsigned opNum = 0; opNum < op->getNumOperands(); ++opNum) {
892 Value val = op->getOperand(opNum);
893 SourceRefLatticeValue refSet = getSourceRefState(val);
894 if (refSet.isSingleValue()) {
895 operandRefs.push_back(refSet.getSingleValue());
896 } else {
897 operandRefs.push_back(std::nullopt);
898 }
899 // First, lookup the operand value after it is initialized
900 auto priorState = operands[opNum]->getValue();
901 if (priorState.getScalarValue().getExpr() != nullptr) {
902 operandVals.push_back(priorState);
903 continue;
904 }
905
906 if (auto readArr = llvm::dyn_cast_if_present<ReadArrayOp>(val.getDefiningOp())) {
907 auto arrayRef = getArrayAccessRef(op, readArr);
908 if (succeeded(arrayRef)) {
909 if (auto it = writeResults.find(*arrayRef); it != writeResults.end()) {
910 operandVals.emplace_back(it->second);
911 Lattice *operandLattice = getLatticeElement(val);
912 (void)operandLattice->setValue(it->second);
913 continue;
914 }
915 }
916 }
917
918 // Else, look up the stored value by `SourceRef`.
919 // We only care about scalar type values here, so ignore aggregate storage values.
920 Type valTy = val.getType();
921 if (llvm::isa<ArrayType, StructType, pod::PodType>(valTy)) {
922 ExpressionValue anyVal(field.get(), createSymbol(valTy, buildStringViaPrint(val).c_str()));
923 operandVals.emplace_back(anyVal);
924 continue;
925 }
926
927 auto resolvedValue = resolveRefStateValue(val, refSet);
928 if (!resolvedValue.has_value()) {
929 // We still return success so we can return overapproximated and partial
930 // results to the user.
931 return success();
932 }
933 operandVals.push_back(*resolvedValue);
934
935 // Since we initialized a value that was not found in the before lattice,
936 // update that value in the lattice so we can find it later, but we don't
937 // need to propagate the changes, since we already have what we need.
938 Lattice *operandLattice = getLatticeElement(val);
939 (void)operandLattice->setValue(operandVals[opNum]);
940 }
941
942 if (isReadOp(op) && op->getNumResults() == 1) {
943 Value resultVal = op->getResult(0);
944 if (!llvm::isa<ArrayType, StructType, pod::PodType>(resultVal.getType())) {
945 auto resolvedValue = resolveRefStateValue(resultVal, getSourceRefState(resultVal));
946 if (resolvedValue.has_value()) {
947 propagateIfChanged(results[0], results[0]->setValue(*resolvedValue));
948 }
949 }
950 return success();
951 }
952
953 // Now, the way we update is dependent on the type of the operation.
954 if (isConstOp(op)) {
955 llvm::DynamicAPInt constVal = getConst(op);
956 llvm::SMTExprRef expr;
957 if (isBoolConstOp(op)) {
958 expr = createConstBoolExpr(constVal != 0);
959 } else {
960 expr = createConstBitvectorExpr(constVal);
961 }
962
963 ExpressionValue latticeVal(field.get(), expr, constVal);
964 if (trackUnreducedIntervals) {
965 latticeVal = latticeVal.withUnreducedInterval(UnreducedInterval(constVal, constVal));
966 }
967 propagateIfChanged(results[0], results[0]->setValue(latticeVal));
968 } else if (isArithmeticOp(op)) {
969 ExpressionValue result;
970 if (operands.size() == 2) {
971 result = performBinaryArithmetic(op, operandVals[0], operandVals[1]);
972 } else {
973 result = performUnaryArithmetic(op, operandVals[0]);
974 }
975
976 // Also intersect with prior interval, if it's initialized
977 const ExpressionValue &prior = results[0]->getValue().getScalarValue();
978 if (prior.getExpr()) {
979 result = refineReducedInterval(result, result.getInterval().intersect(prior.getInterval()));
980 }
981 propagateIfChanged(results[0], results[0]->setValue(result));
982 } else if (auto selectOp = llvm::dyn_cast<arith::SelectOp>(op)) {
984 smtSolver, operandVals[0].getScalarValue(), operandVals[1].getScalarValue(),
985 operandVals[2].getScalarValue()
986 );
987 const ExpressionValue &prior = results[0]->getValue().getScalarValue();
988 if (prior.getExpr()) {
989 result = refineReducedInterval(result, result.getInterval().intersect(prior.getInterval()));
990 }
991 propagateIfChanged(results[0], results[0]->setValue(result));
992 } else if (EmitEqualityOp emitEq = llvm::dyn_cast<EmitEqualityOp>(op)) {
993 Value lhsVal = emitEq.getLhs(), rhsVal = emitEq.getRhs();
994 ExpressionValue lhsExpr = operandVals[0].getScalarValue();
995 ExpressionValue rhsExpr = operandVals[1].getScalarValue();
996
997 // Special handling for generalized (s - c0) * (s - c1) * ... * (s - cN) = 0 patterns.
998 // These patterns enforce that s is one of c0, ..., cN.
999 auto res = getGeneralizedDecompInterval(op, lhsVal, rhsVal);
1000 if (succeeded(res)) {
1001 for (Value signalVal : res->first) {
1002 applyInterval(emitEq, signalVal, res->second);
1003 }
1004 }
1005
1006 ExpressionValue constraint = intersection(smtSolver, lhsExpr, rhsExpr);
1007 // Update the LHS and RHS to the same value, but restricted intervals
1008 // based on the constraints.
1009 const Interval &constrainInterval = constraint.getInterval();
1010 applyInterval(emitEq, lhsVal, constrainInterval);
1011 applyInterval(emitEq, rhsVal, constrainInterval);
1012 } else if (auto assertOp = llvm::dyn_cast<AssertOp>(op)) {
1013 // assert enforces that the operand is true. So we apply an interval of [1, 1]
1014 // to the operand.
1015 Value cond = assertOp.getCondition();
1016 applyInterval(assertOp, cond, Interval::True(field.get()));
1017 // Also add the solver constraint that the expression must be true.
1018 auto assertExpr = operandVals[0].getScalarValue();
1019 // No need to propagate the constraint
1020 (void)getLatticeElement(cond)->addSolverConstraint(assertExpr);
1021 } else if (auto writePod = llvm::dyn_cast<pod::WritePodOp>(op)) {
1022 const bool maySkipWrite = isInMaybeSkippedScfRegion(op);
1023 SourceRefLatticeValue podRefs = getSourceRefState(writePod.getPodRef());
1024 if (podRefs.isScalar()) {
1025 auto recordRefsRes = podRefs.referencePodRecord(writePod.getRecordNameAttr());
1026 ensure(succeeded(recordRefsRes), "could not create SourceRef child for pod write");
1027 SourceRefLatticeValue recordRefs = recordRefsRes->first;
1028 Type valueTy = writePod.getValue().getType();
1029 if (!llvm::isa<ArrayType, StructType, pod::PodType>(valueTy)) {
1030 ExpressionValue writeVal = operandVals[1].getScalarValue();
1031 for (const SourceRef &recordRef : recordRefs.getScalarValue()) {
1032 recordRefWrite(recordRef, writeVal, maySkipWrite);
1033 }
1034 } else if (operandRefs[1].has_value()) {
1035 llvm::SmallVector<std::pair<SourceRef, ExpressionValue>> remappedWrites;
1036 for (const SourceRef &recordRef : recordRefs.getScalarValue()) {
1037 for (const auto &[writtenRef, writtenVal] : writeResults) {
1038 if (writtenRef.isValidPrefix(*operandRefs[1])) {
1039 auto translated = writtenRef.translate(*operandRefs[1], recordRef);
1040 ensure(succeeded(translated), "could not translate aggregate pod write");
1041 remappedWrites.emplace_back(*translated, writtenVal);
1042 }
1043 }
1044 }
1045 for (const auto &[translatedRef, translatedVal] : remappedWrites) {
1046 recordRefWrite(translatedRef, translatedVal, maySkipWrite);
1047 }
1048 }
1049 }
1050 } else if (auto writem = llvm::dyn_cast<MemberWriteOp>(op)) {
1051 const bool maySkipWrite = isInMaybeSkippedScfRegion(op);
1052 // Update values stored in a member
1053 ExpressionValue writeVal = operandVals[1].getScalarValue();
1054 auto cmp = writem.getComponent();
1055 // We also need to update the interval on the assigned symbol
1056 SourceRefLatticeValue refSet = getSourceRefState(cmp);
1057 if (refSet.isSingleValue()) {
1058 auto memberDefRes = writem.getMemberDefOp(tables);
1059 if (succeeded(memberDefRes)) {
1060 SourceRefIndex idx(memberDefRes.value());
1061 auto memberRefRes = refSet.getSingleValue().createChild(idx);
1062 ensure(succeeded(memberRefRes), "could not create SourceRef child for member write");
1063 const SourceRef &memberRef = *memberRefRes;
1064 Type memberTy = writem.getVal().getType();
1065 if (!llvm::isa<ArrayType, StructType, pod::PodType>(memberTy)) {
1066 // Simple scalar update
1067 recordRefWrite(memberRef, writeVal, maySkipWrite);
1068 } else {
1069 // Map the intervals of aggregates to the written member
1070 std::optional<SourceRef> rhsPrefix;
1071 if (operandRefs[1].has_value() && operandRefs[1]->isRooted()) {
1072 rhsPrefix = operandRefs[1];
1073 } else if (auto blockArg = llvm::dyn_cast<BlockArgument>(writem.getVal())) {
1074 rhsPrefix = SourceRef(blockArg);
1075 } else if (auto result = llvm::dyn_cast<OpResult>(writem.getVal())) {
1076 rhsPrefix = SourceRef(result);
1077 }
1078
1079 if (rhsPrefix.has_value()) {
1080 llvm::SmallVector<std::pair<SourceRef, ExpressionValue>> remappedWrites;
1081 for (const auto &[writtenRef, writtenVal] : writeResults) {
1082 if (!writtenRef.isValidPrefix(*rhsPrefix)) {
1083 continue;
1084 }
1085
1086 auto translatedRef = writtenRef.translate(*rhsPrefix, memberRef);
1087 ensure(succeeded(translatedRef), "could not translate composite member write");
1088 remappedWrites.emplace_back(*translatedRef, writtenVal);
1089 }
1090
1091 for (const auto &[translatedRef, translatedVal] : remappedWrites) {
1092 recordRefWrite(translatedRef, translatedVal, maySkipWrite);
1093 }
1094 }
1095 }
1096 }
1097 }
1098 } else if (auto writeArr = llvm::dyn_cast<WriteArrayOp>(op)) {
1099 const bool maySkipWrite = isInMaybeSkippedScfRegion(op);
1100 ExpressionValue writeVal = operandVals.back().getScalarValue();
1101 auto arrayRef = getArrayAccessRef(op, writeArr);
1102 if (succeeded(arrayRef)) {
1103 recordRefWrite(*arrayRef, writeVal, maySkipWrite);
1104 }
1105
1106 SourceRefLatticeValue arrayVals = getSourceRefState(writeArr.getArrRef());
1107 if (arrayVals.isScalar()) {
1108 std::vector<SourceRefIndex> indices = getArrayAccessIndices(op, writeArr);
1109 auto targetRefsRes = arrayVals.extract(indices);
1110 ensure(succeeded(targetRefsRes), "could not create SourceRef child for array write");
1111 auto [targetRefs, _] = *targetRefsRes;
1112 ensure(targetRefs.isScalar(), "array write must resolve to scalar references");
1113 for (const SourceRef &ref : targetRefs.getScalarValue()) {
1114 recordRefWrite(ref, writeVal, maySkipWrite);
1115 }
1116 }
1117 } else if (auto createArray = llvm::dyn_cast<CreateArrayOp>(op)) {
1118 const auto &elements = createArray.getElements();
1119 ArrayType arrayTy = createArray.getType();
1120 Type elemTy = arrayTy.getElementType();
1121
1122 if (!elements.empty() && !llvm::isa<ArrayType, StructType, pod::PodType>(elemTy)) {
1123 ensure(arrayTy.hasStaticShape(), "array.new with explicit elements must have static shape");
1124 ensure(
1125 std::cmp_equal(elements.size(), arrayTy.getNumElements()),
1126 "array.new explicit initializer length must match array shape"
1127 );
1128
1130 auto arrayRes = llvm::cast<OpResult>(createArray->getResult(0));
1131 for (unsigned i = 0; i < elements.size(); ++i) {
1132 auto maybeIndices = indexGen.delinearize(i, op->getContext());
1133 ensure(maybeIndices.has_value(), "could not delinearize array.new element index");
1134
1135 SourceRef::Path path;
1136 path.reserve(maybeIndices->size());
1137 for (Attribute attr : *maybeIndices) {
1138 auto idxAttr = llvm::dyn_cast<IntegerAttr>(attr);
1139 ensure(idxAttr != nullptr, "array.new delinearize should produce integer attributes");
1140 path.emplace_back(idxAttr.getValue());
1141 }
1142
1143 recordRefWrite(SourceRef(arrayRes, std::move(path)), operandVals[i].getScalarValue());
1144 }
1145 } else if (!elements.empty()) {
1146 ensure(arrayTy.hasStaticShape(), "aggregate array.new initializer requires static shape");
1148 SourceRef arrayRoot(llvm::cast<OpResult>(createArray.getResult()));
1149 llvm::SmallVector<std::pair<SourceRef, ExpressionValue>> remappedWrites;
1150 for (auto [i, element] : llvm::enumerate(elements)) {
1151 SourceRefLatticeValue elementRefs = getSourceRefState(element);
1152 if (!elementRefs.isSingleValue()) {
1153 continue;
1154 }
1155 auto maybeIndices = indexGen.delinearize(i, op->getContext());
1156 ensure(maybeIndices.has_value(), "could not delinearize aggregate array.new index");
1157 SourceRef elementTarget = arrayRoot;
1158 for (Attribute attr : *maybeIndices) {
1159 auto child =
1160 elementTarget.createChild(SourceRefIndex(llvm::cast<IntegerAttr>(attr).getValue()));
1161 ensure(succeeded(child), "could not create aggregate array element SourceRef");
1162 elementTarget = *child;
1163 }
1164 for (const auto &[writtenRef, writtenVal] : writeResults) {
1165 if (writtenRef.isValidPrefix(elementRefs.getSingleValue())) {
1166 auto translated = writtenRef.translate(elementRefs.getSingleValue(), elementTarget);
1167 ensure(succeeded(translated), "could not translate aggregate array initializer");
1168 remappedWrites.emplace_back(*translated, writtenVal);
1169 }
1170 }
1171 }
1172 for (const auto &[translatedRef, translatedVal] : remappedWrites) {
1173 recordRefWrite(translatedRef, translatedVal);
1174 }
1175 }
1176 } else if (auto newPod = llvm::dyn_cast<pod::NewPodOp>(op)) {
1177 SourceRef podRoot(llvm::cast<OpResult>(newPod.getResult()));
1178 for (auto [idx, record] : llvm::enumerate(newPod.getInitializedRecordValues())) {
1179 auto recordRef =
1180 podRoot.createChild(SourceRefIndex(StringAttr::get(op->getContext(), record.name)));
1181 ensure(succeeded(recordRef), "could not create SourceRef child for pod initializer");
1182 if (!llvm::isa<ArrayType, StructType, pod::PodType>(record.value.getType())) {
1183 recordRefWrite(*recordRef, operandVals[idx].getScalarValue());
1184 continue;
1185 }
1186
1187 SourceRefLatticeValue sourceRefs = getSourceRefState(record.value);
1188 if (!sourceRefs.isSingleValue()) {
1189 continue;
1190 }
1191 llvm::SmallVector<std::pair<SourceRef, ExpressionValue>> remappedWrites;
1192 for (const auto &[writtenRef, writtenVal] : writeResults) {
1193 if (writtenRef.isValidPrefix(sourceRefs.getSingleValue())) {
1194 auto translated = writtenRef.translate(sourceRefs.getSingleValue(), *recordRef);
1195 ensure(succeeded(translated), "could not translate aggregate pod initializer");
1196 remappedWrites.emplace_back(*translated, writtenVal);
1197 }
1198 }
1199 for (const auto &[translatedRef, translatedVal] : remappedWrites) {
1200 recordRefWrite(translatedRef, translatedVal);
1201 }
1202 }
1203 } else if (isa<IntToFeltOp, FeltToIndexOp>(op)) {
1204 // Casts don't modify the intervals, but they do modify the SMT types.
1205 ExpressionValue expr = operandVals[0].getScalarValue();
1206 // We treat all ints and indexes as felts with the exception of comparison
1207 // results, which are bools. So if `expr` is a bool, this cast needs to
1208 // upcast to a felt.
1209 if (expr.isBoolSort(smtSolver)) {
1210 expr = boolToFelt(smtSolver, expr, field.get().bitWidth());
1211 }
1212 propagateIfChanged(results[0], results[0]->setValue(expr));
1213 } else if (auto yieldOp = dyn_cast<scf::YieldOp>(op)) {
1214 // Fetch the lattice for after the parent operation so we can propagate
1215 // the yielded value to subsequent operations.
1216 Operation *parent = op->getParentOp();
1217 ensure(parent, "yield operation must have parent operation");
1218 // Bind the operand values to the result values of the parent
1219 for (unsigned idx = 0; idx < yieldOp.getResults().size(); ++idx) {
1220 Value parentRes = parent->getResult(idx);
1221 Lattice *resLattice = getLatticeElement(parentRes);
1222 // Merge with the existing value, if present (e.g., another branch)
1223 // has possible value that must be merged.
1224 ExpressionValue exprVal = resLattice->getValue().getScalarValue();
1225 ExpressionValue newResVal = operandVals[idx].getScalarValue();
1226 if (auto loopOp = llvm::dyn_cast<LoopLikeOpInterface>(parent)) {
1227 // We overapproximate for loops because we aren't going to try to track trip count.
1228 newResVal = ExpressionValue(createSymbol(parentRes), Interval::Entire(field.get()));
1229 }
1230 if (exprVal.getExpr() != nullptr) {
1231 newResVal =
1232 exprVal.withInterval(exprVal.getInterval().join(newResVal.getInterval()))
1233 .withOptionalUnreducedInterval(mergeUnreducedIntervals(
1235 ));
1236 } else {
1237 newResVal = ExpressionValue(
1238 createSymbol(parentRes), newResVal.getInterval(),
1240 );
1241 }
1242 propagateIfChanged(resLattice, resLattice->setValue(newResVal));
1243 }
1244 } else if (
1245 // We do not need to explicitly handle read ops since they are resolved at the operand value
1246 // step where `SourceRef`s are queries.
1247 !isReadOp(op)
1248 // We do not currently handle return ops as the analysis is currently limited to constrain
1249 // functions, which return no value.
1250 && !isReturnOp(op)
1251 // The analysis ignores definition ops.
1252 && !isDefinitionOp(op)
1253 // We do not need to analyze storage creation directly.
1254 && !llvm::isa<CreateArrayOp, CreateStructOp, pod::NewPodOp, NonDetOp>(op)
1255 ) {
1256 op->emitWarning("unhandled operation, analysis may be incomplete").report();
1257 }
1258
1259 return success();
1260}
1261
1263 auto it = refSymbols.find(r);
1264 if (it != refSymbols.end()) {
1265 return it->second;
1266 }
1267 llvm::SMTExprRef sym = createSymbol(r);
1268 refSymbols[r] = sym;
1269 return sym;
1270}
1271
1272llvm::SMTExprRef IntervalDataFlowAnalysis::createSymbol(mlir::Type ty, const char *name) const {
1273 if (isBooleanType(ty)) {
1274 return smtSolver->mkSymbol(name, smtSolver->getBoolSort());
1275 }
1276 return field.get().createSymbol(smtSolver, name);
1277}
1278
1279llvm::SMTExprRef IntervalDataFlowAnalysis::createSymbol(const SourceRef &r) const {
1280 std::string name = buildStringViaPrint(r);
1281 return createSymbol(r.getType(), name.c_str());
1282}
1283
1284llvm::SMTExprRef IntervalDataFlowAnalysis::createSymbol(Value v) const {
1285 std::string name = buildStringViaPrint(v);
1286 return createSymbol(v.getType(), name.c_str());
1287}
1288
1289llvm::DynamicAPInt IntervalDataFlowAnalysis::getConst(Operation *op) const {
1290 ensure(isConstOp(op), "op is not a const op");
1291
1292 // NOTE: I think clang-format makes these hard to read by default
1293 // clang-format off
1294 llvm::DynamicAPInt fieldConst = TypeSwitch<Operation *, llvm::DynamicAPInt>(op)
1295 .Case<FeltConstantOp>([&](auto feltConst) {
1296 llvm::APSInt constOpVal(feltConst.getValue());
1297 return field.get().reduce(constOpVal);
1298 })
1299 .Case<arith::ConstantIndexOp>([&](auto indexConst) {
1300 return DynamicAPInt(indexConst.value());
1301 })
1302 .Case<arith::ConstantIntOp>([&](auto intConst) {
1303 auto valAttr = dyn_cast<IntegerAttr>(intConst.getValue());
1304 ensure(valAttr != nullptr, "arith::ConstantIntOp must have an IntegerAttr as its value");
1305 return toDynamicAPInt(valAttr.getValue());
1306 })
1307 .Default([](auto *illegalOp) {
1308 std::string err;
1309 debug::Appender(err) << "unhandled getConst case: " << *illegalOp;
1310 llvm::report_fatal_error(Twine(err));
1311 return llvm::DynamicAPInt();
1312 });
1313 // clang-format on
1314 return fieldConst;
1315}
1316
1317ExpressionValue IntervalDataFlowAnalysis::performBinaryArithmetic(
1318 Operation *op, const LatticeValue &a, const LatticeValue &b
1319) {
1320 ensure(isArithmeticOp(op), "is not arithmetic op");
1321
1322 auto lhs = a.getScalarValue(), rhs = b.getScalarValue();
1323 ensure(lhs.getExpr(), "cannot perform arithmetic over null lhs smt expr");
1324 ensure(rhs.getExpr(), "cannot perform arithmetic over null rhs smt expr");
1325
1326 // clang-format off
1327 auto res = TypeSwitch<Operation *, ExpressionValue>(op)
1328 .Case<AddFeltOp>([&](auto) { return add(smtSolver, lhs, rhs); })
1329 .Case<SubFeltOp>([&](auto) { return sub(smtSolver, lhs, rhs); })
1330 .Case<MulFeltOp>([&](auto) { return mul(smtSolver, lhs, rhs); })
1331 .Case<DivFeltOp>([&](auto) {return div(smtSolver, op, lhs, rhs); })
1332 .Case<UnsignedIntDivFeltOp>([&](auto) {return uintDiv(smtSolver, op, lhs, rhs); })
1333 .Case<SignedIntDivFeltOp>([&](auto) {return sintDiv(smtSolver, op, lhs, rhs); })
1334 .Case<UnsignedModFeltOp>([&](auto) { return mod(smtSolver, lhs, rhs); })
1335 .Case<SignedModFeltOp>([&](auto) { return sintMod(smtSolver, lhs, rhs); })
1336 .Case<AndFeltOp>([&](auto) { return bitAnd(smtSolver, lhs, rhs); })
1337 .Case<OrFeltOp>([&](auto) { return bitOr(smtSolver, lhs, rhs); })
1338 .Case<XorFeltOp, arith::XOrIOp>([&](auto) { return bitXor(smtSolver, lhs, rhs); })
1339 .Case<ShlFeltOp>([&](auto) { return shiftLeft(smtSolver, lhs, rhs); })
1340 .Case<ShrFeltOp>([&](auto) { return shiftRight(smtSolver, lhs, rhs); })
1341 .Case<CmpOp>([&](auto cmpOp) { return cmp(smtSolver, cmpOp, lhs, rhs); })
1342 .Case<AndBoolOp>([&](auto) { return boolAnd(smtSolver, lhs, rhs); })
1343 .Case<OrBoolOp>([&](auto) { return boolOr(smtSolver, lhs, rhs); })
1344 .Case<XorBoolOp>([&](auto) { return boolXor(smtSolver, lhs, rhs); })
1345 .Default([&](auto *unsupported) {
1346 unsupported
1347 ->emitError(
1348 "unsupported binary arithmetic operation"
1349 )
1350 .report();
1351 return ExpressionValue();
1352 });
1353 // clang-format on
1354
1355 ensure(res.getExpr(), "arithmetic produced null smt expr");
1356 return res;
1357}
1358
1360IntervalDataFlowAnalysis::performUnaryArithmetic(Operation *op, const LatticeValue &a) {
1361 ensure(isArithmeticOp(op), "is not arithmetic op");
1362
1363 auto val = a.getScalarValue();
1364 ensure(val.getExpr(), "cannot perform arithmetic over null smt expr");
1365
1366 auto res = TypeSwitch<Operation *, ExpressionValue>(op)
1367 .Case<NegFeltOp>([&](auto) { return neg(smtSolver, val); })
1368 .Case<NotFeltOp>([&](auto) { return notOp(smtSolver, val); })
1369 .Case<NotBoolOp>([&](auto) { return boolNot(smtSolver, val); })
1370 // The inverse op is currently overapproximated
1371 .Case<InvFeltOp>([&](auto inv) {
1372 return fallbackUnaryOp(smtSolver, inv, val);
1373 }).Default([&](auto *unsupported) {
1374 unsupported
1375 ->emitWarning(
1376 "unsupported unary arithmetic operation, defaulting to over-approximated interval"
1377 )
1378 .report();
1379 return fallbackUnaryOp(smtSolver, unsupported, val);
1380 });
1381
1382 ensure(res.getExpr(), "arithmetic produced null smt expr");
1383 return res;
1384}
1385
1386void IntervalDataFlowAnalysis::applyInterval(Operation *valUser, Value val, Interval newInterval) {
1387 Lattice *valLattice = getLatticeElement(val);
1388 ExpressionValue oldLatticeVal = valLattice->getValue().getScalarValue();
1389 // Intersect with the current value to accumulate restrictions across constraints.
1390 Interval intersection = oldLatticeVal.getInterval().intersect(newInterval);
1391 ExpressionValue newLatticeVal = refineReducedInterval(oldLatticeVal, intersection);
1392 ChangeResult changed = valLattice->setValue(newLatticeVal);
1393
1394 if (auto blockArg = llvm::dyn_cast<BlockArgument>(val)) {
1395 auto fnOp = dyn_cast<FuncDefOp>(blockArg.getOwner()->getParentOp());
1396
1397 // Apply the interval from the constrain function inputs to the compute function inputs
1398 if (propagateInputConstraints && fnOp && fnOp.isStructConstrain() &&
1399 blockArg.getArgNumber() > 0 && !newInterval.isEntire()) {
1400 auto structOp = fnOp->getParentOfType<StructDefOp>();
1401 FuncDefOp computeFn = structOp.getComputeFuncOp();
1402 BlockArgument computeArg = computeFn.getArgument(blockArg.getArgNumber() - 1);
1403 Lattice *computeEntryLattice = getLatticeElement(computeArg);
1404
1405 SourceRef ref(computeArg);
1406 ExpressionValue newArgVal(
1407 getOrCreateSymbol(ref), newInterval,
1408 trackUnreducedIntervals ? std::optional<UnreducedInterval>(newInterval.firstUnreduced())
1409 : std::nullopt
1410 );
1411 propagateIfChanged(computeEntryLattice, computeEntryLattice->setValue(newArgVal));
1412 }
1413 }
1414
1415 // Now we descend into val's operands, if it has any.
1416 Operation *definingOp = val.getDefiningOp();
1417 if (!definingOp) {
1418 propagateIfChanged(valLattice, changed);
1419 return;
1420 }
1421
1422 const Field &f = field.get();
1423
1424 // This is a rules-based operation. If we have a rule for a given operation,
1425 // then we can make some kind of update, otherwise we leave the intervals
1426 // as is.
1427 // - First we'll define all the rules so the type switch can be less messy
1428
1429 // cmp.<pred> restricts each side of the comparison if the result is known.
1430 auto cmpCase = [&](CmpOp cmpOp) {
1431 // Cmp output range is [0, 1], so in order to do something, we must have newInterval
1432 // either "true" (1) or "false" (0).
1433 // -- In the case of a contradictory circuit, however, the cmp result is allowed
1434 // to be empty.
1435 ensure(
1436 newInterval.isBoolean() || newInterval.isEmpty(),
1437 "new interval for CmpOp is not boolean or empty"
1438 );
1439 if (!newInterval.isDegenerate()) {
1440 // The comparison result is unknown, so we can't update the operand ranges
1441 return;
1442 }
1443
1444 bool cmpTrue = newInterval.rhs() == f.one();
1445
1446 Value lhs = cmpOp.getLhs(), rhs = cmpOp.getRhs();
1447 auto lhsLat = getLatticeElement(lhs), rhsLat = getLatticeElement(rhs);
1448 ExpressionValue lhsExpr = lhsLat->getValue().getScalarValue(),
1449 rhsExpr = rhsLat->getValue().getScalarValue();
1450
1451 Interval newLhsInterval, newRhsInterval;
1452 const Interval &lhsInterval = lhsExpr.getInterval();
1453 const Interval &rhsInterval = rhsExpr.getInterval();
1454
1455 FeltCmpPredicate pred = cmpOp.getPredicate();
1456 // predicate cases
1457 auto eqCase = [&]() {
1458 return (pred == FeltCmpPredicate::EQ && cmpTrue) ||
1459 (pred == FeltCmpPredicate::NE && !cmpTrue);
1460 };
1461 auto neCase = [&]() {
1462 return (pred == FeltCmpPredicate::NE && cmpTrue) ||
1463 (pred == FeltCmpPredicate::EQ && !cmpTrue);
1464 };
1465 auto ltCase = [&]() {
1466 return (pred == FeltCmpPredicate::LT && cmpTrue) ||
1467 (pred == FeltCmpPredicate::GE && !cmpTrue);
1468 };
1469 auto leCase = [&]() {
1470 return (pred == FeltCmpPredicate::LE && cmpTrue) ||
1471 (pred == FeltCmpPredicate::GT && !cmpTrue);
1472 };
1473 auto gtCase = [&]() {
1474 return (pred == FeltCmpPredicate::GT && cmpTrue) ||
1475 (pred == FeltCmpPredicate::LE && !cmpTrue);
1476 };
1477 auto geCase = [&]() {
1478 return (pred == FeltCmpPredicate::GE && cmpTrue) ||
1479 (pred == FeltCmpPredicate::LT && !cmpTrue);
1480 };
1481
1482 // new intervals based on case
1483 if (eqCase()) {
1484 newLhsInterval = newRhsInterval = lhsInterval.intersect(rhsInterval);
1485 } else if (neCase()) {
1486 if (lhsInterval.isDegenerate() && rhsInterval.isDegenerate() && lhsInterval == rhsInterval) {
1487 // In this case, we know lhs and rhs cannot satisfy this assertion, so they have
1488 // an empty value range.
1489 newLhsInterval = newRhsInterval = Interval::Empty(f);
1490 } else if (lhsInterval.isDegenerate()) {
1491 // rhs must not overlap with lhs
1492 newLhsInterval = lhsInterval;
1493 newRhsInterval = rhsInterval.difference(lhsInterval);
1494 } else if (rhsInterval.isDegenerate()) {
1495 // lhs must not overlap with rhs
1496 newLhsInterval = lhsInterval.difference(rhsInterval);
1497 newRhsInterval = rhsInterval;
1498 } else {
1499 // Leave unchanged
1500 newLhsInterval = lhsInterval;
1501 newRhsInterval = rhsInterval;
1502 }
1503 } else if (ltCase()) {
1504 newLhsInterval = lhsInterval.toUnreduced().computeLTPart(rhsInterval.toUnreduced()).reduce(f);
1505 newRhsInterval = rhsInterval.toUnreduced().computeGEPart(lhsInterval.toUnreduced()).reduce(f);
1506 } else if (leCase()) {
1507 newLhsInterval = lhsInterval.toUnreduced().computeLEPart(rhsInterval.toUnreduced()).reduce(f);
1508 newRhsInterval = rhsInterval.toUnreduced().computeGTPart(lhsInterval.toUnreduced()).reduce(f);
1509 } else if (gtCase()) {
1510 newLhsInterval = lhsInterval.toUnreduced().computeGTPart(rhsInterval.toUnreduced()).reduce(f);
1511 newRhsInterval = rhsInterval.toUnreduced().computeLEPart(lhsInterval.toUnreduced()).reduce(f);
1512 } else if (geCase()) {
1513 newLhsInterval = lhsInterval.toUnreduced().computeGEPart(rhsInterval.toUnreduced()).reduce(f);
1514 newRhsInterval = rhsInterval.toUnreduced().computeLTPart(lhsInterval.toUnreduced()).reduce(f);
1515 } else {
1516 cmpOp->emitWarning("unhandled cmp predicate").report();
1517 return;
1518 }
1519
1520 // Now we recurse to each operand
1521 applyInterval(cmpOp, lhs, newLhsInterval);
1522 applyInterval(cmpOp, rhs, newRhsInterval);
1523 };
1524
1525 // Multiplication cases:
1526 // - If the result of a multiplication is non-zero, then both operands must be
1527 // non-zero.
1528 // - If one operand is a constant, we can propagate the new interval when multiplied
1529 // by the multiplicative inverse of the constant.
1530 auto mulCase = [&](MulFeltOp mulOp) {
1531 // We check for the constant case first.
1532 auto constCase = [&](FeltConstantOp constOperand, Value multiplicand) {
1533 auto latVal = getLatticeElement(multiplicand)->getValue().getScalarValue();
1534 APInt constVal = constOperand.getValue();
1535 if (constVal.isZero()) {
1536 // There's no inverse for zero, so we do nothing.
1537 return;
1538 }
1539 Interval updatedInterval = newInterval * Interval::Degenerate(f, f.inv(constVal));
1540 applyInterval(mulOp, multiplicand, updatedInterval);
1541 };
1542
1543 Value lhs = mulOp.getLhs(), rhs = mulOp.getRhs();
1544
1545 auto lhsConstOp = dyn_cast_if_present<FeltConstantOp>(lhs.getDefiningOp());
1546 auto rhsConstOp = dyn_cast_if_present<FeltConstantOp>(rhs.getDefiningOp());
1547 // If both are consts, we don't need to do anything
1548 if (lhsConstOp && rhsConstOp) {
1549 return;
1550 } else if (lhsConstOp) {
1551 constCase(lhsConstOp, rhs);
1552 return;
1553 } else if (rhsConstOp) {
1554 constCase(rhsConstOp, lhs);
1555 return;
1556 }
1557
1558 // Otherwise, try to propagate non-zero information.
1559 auto zeroInt = Interval::Degenerate(f, f.zero());
1560 if (newInterval.intersect(zeroInt).isNotEmpty()) {
1561 // The multiplication may be zero, so we can't reduce the operands to be non-zero
1562 return;
1563 }
1564
1565 auto lhsLat = getLatticeElement(lhs), rhsLat = getLatticeElement(rhs);
1566 ExpressionValue lhsExpr = lhsLat->getValue().getScalarValue(),
1567 rhsExpr = rhsLat->getValue().getScalarValue();
1568 Interval newLhsInterval = lhsExpr.getInterval().difference(zeroInt);
1569 Interval newRhsInterval = rhsExpr.getInterval().difference(zeroInt);
1570 applyInterval(mulOp, lhs, newLhsInterval);
1571 applyInterval(mulOp, rhs, newRhsInterval);
1572 };
1573
1574 auto addCase = [&](AddFeltOp addOp) {
1575 Value lhs = addOp.getLhs(), rhs = addOp.getRhs();
1576 Lattice *lhsLat = getLatticeElement(lhs), *rhsLat = getLatticeElement(rhs);
1577 ExpressionValue lhsVal = lhsLat->getValue().getScalarValue();
1578 ExpressionValue rhsVal = rhsLat->getValue().getScalarValue();
1579
1580 const Interval &currLhsInt = lhsVal.getInterval(), &currRhsInt = rhsVal.getInterval();
1581
1582 Interval derivedLhsInt = newInterval - currRhsInt;
1583 Interval derivedRhsInt = newInterval - currLhsInt;
1584
1585 Interval finalLhsInt = currLhsInt.intersect(derivedLhsInt);
1586 Interval finalRhsInt = currRhsInt.intersect(derivedRhsInt);
1587
1588 applyInterval(addOp, lhs, finalLhsInt);
1589 applyInterval(addOp, rhs, finalRhsInt);
1590 };
1591
1592 auto subCase = [&](SubFeltOp subOp) {
1593 Value lhs = subOp.getLhs(), rhs = subOp.getRhs();
1594 Lattice *lhsLat = getLatticeElement(lhs), *rhsLat = getLatticeElement(rhs);
1595 ExpressionValue lhsVal = lhsLat->getValue().getScalarValue();
1596 ExpressionValue rhsVal = rhsLat->getValue().getScalarValue();
1597
1598 const Interval &currLhsInt = lhsVal.getInterval(), &currRhsInt = rhsVal.getInterval();
1599
1600 Interval derivedLhsInt = newInterval + currRhsInt;
1601 Interval derivedRhsInt = currLhsInt - newInterval;
1602
1603 Interval finalLhsInt = currLhsInt.intersect(derivedLhsInt);
1604 Interval finalRhsInt = currRhsInt.intersect(derivedRhsInt);
1605
1606 applyInterval(subOp, lhs, finalLhsInt);
1607 applyInterval(subOp, rhs, finalRhsInt);
1608 };
1609
1610 auto selectCase = [&](arith::SelectOp selectOp) {
1611 Value cond = selectOp.getCondition();
1612 Value trueVal = selectOp.getTrueValue();
1613 Value falseVal = selectOp.getFalseValue();
1614
1615 ExpressionValue condExpr = getLatticeElement(cond)->getValue().getScalarValue();
1616 ExpressionValue trueExpr = getLatticeElement(trueVal)->getValue().getScalarValue();
1617 ExpressionValue falseExpr = getLatticeElement(falseVal)->getValue().getScalarValue();
1618
1619 const Interval &condInterval = condExpr.getInterval();
1620 if (condInterval.isDegenerate() && condInterval.rhs() == f.one()) {
1621 applyInterval(selectOp, trueVal, newInterval);
1622 return;
1623 }
1624 if (condInterval.isDegenerate() && condInterval.rhs() == f.zero()) {
1625 applyInterval(selectOp, falseVal, newInterval);
1626 return;
1627 }
1628
1629 Interval trueOverlap = trueExpr.getInterval().intersect(newInterval);
1630 Interval falseOverlap = falseExpr.getInterval().intersect(newInterval);
1631 bool truePossible = trueOverlap.isNotEmpty();
1632 bool falsePossible = falseOverlap.isNotEmpty();
1633
1634 if (truePossible && !falsePossible) {
1635 applyInterval(selectOp, cond, Interval::True(f));
1636 applyInterval(selectOp, trueVal, newInterval);
1637 return;
1638 }
1639 if (!truePossible && falsePossible) {
1640 applyInterval(selectOp, cond, Interval::False(f));
1641 applyInterval(selectOp, falseVal, newInterval);
1642 return;
1643 }
1644 if (!truePossible && !falsePossible) {
1645 applyInterval(selectOp, cond, Interval::Empty(f));
1646 }
1647 };
1648
1649 auto readmCase = [&](MemberReadOp) {
1650 SourceRefLatticeValue sourceRefVal = getSourceRefState(val);
1651
1652 if (sourceRefVal.isSingleValue()) {
1653 const SourceRef &ref = sourceRefVal.getSingleValue();
1654 readResults[ref].insert(valLattice);
1655
1656 // Also propagate to all other member read results for this member
1657 for (Lattice *l : readResults[ref]) {
1658 if (l != valLattice) {
1659 propagateIfChanged(l, l->setValue(newLatticeVal));
1660 }
1661 }
1662 }
1663 };
1664
1665 auto readArrCase = [&](ReadArrayOp) {
1666 auto arrayRef = getArrayAccessRef(valUser, llvm::cast<ReadArrayOp>(definingOp));
1667 if (succeeded(arrayRef)) {
1668 readResults[*arrayRef].insert(valLattice);
1669
1670 for (Lattice *l : readResults[*arrayRef]) {
1671 if (l != valLattice) {
1672 propagateIfChanged(l, l->setValue(newLatticeVal));
1673 }
1674 }
1675 }
1676
1677 SourceRefLatticeValue sourceRefVal = getSourceRefState(val);
1678
1679 if (sourceRefVal.isSingleValue()) {
1680 const SourceRef &ref = sourceRefVal.getSingleValue();
1681 readResults[ref].insert(valLattice);
1682
1683 // Also propagate to all other member read results for this member
1684 for (Lattice *l : readResults[ref]) {
1685 if (l != valLattice) {
1686 propagateIfChanged(l, l->setValue(newLatticeVal));
1687 }
1688 }
1689 }
1690 };
1691
1692 // For casts, just pass the interval along to the cast's operand.
1693 auto castCase = [&](Operation *op) { applyInterval(op, op->getOperand(0), newInterval); };
1694
1695 // - Apply the rules given the op.
1696 // NOTE: disabling clang-format for this because it makes the last case statement
1697 // look ugly.
1698 // clang-format off
1699 TypeSwitch<Operation *>(definingOp)
1700 .Case<CmpOp>([&](auto op) { cmpCase(op); })
1701 .Case<AddFeltOp>([&](auto op) { return addCase(op); })
1702 .Case<SubFeltOp>([&](auto op) { return subCase(op); })
1703 .Case<MulFeltOp>([&](auto op) { mulCase(op); })
1704 .Case<arith::SelectOp>([&](auto op) { selectCase(op); })
1705 .Case<MemberReadOp>([&](auto op){ readmCase(op); })
1706 .Case<ReadArrayOp>([&](auto op){ readArrCase(op); })
1707 .Case<IntToFeltOp, FeltToIndexOp>([&](auto op) { castCase(op); })
1708 .Default([&](Operation *) { });
1709 // clang-format on
1710
1711 // Propagate after recursion to avoid having recursive calls unset the value.
1712 propagateIfChanged(valLattice, changed);
1713}
1714
1715FailureOr<std::pair<DenseSet<Value>, Interval>>
1716IntervalDataFlowAnalysis::getGeneralizedDecompInterval(
1717 Operation * /*baseOp*/, Value lhs, Value rhs
1718) {
1719 auto isZeroConst = [this](Value v) {
1720 Operation *op = v.getDefiningOp();
1721 if (!op) {
1722 return false;
1723 }
1724 if (!isConstOp(op)) {
1725 return false;
1726 }
1727 return getConst(op) == field.get().zero();
1728 };
1729 bool lhsIsZero = isZeroConst(lhs), rhsIsZero = isZeroConst(rhs);
1730 Value exprTree = nullptr;
1731 if (lhsIsZero && !rhsIsZero) {
1732 exprTree = rhs;
1733 } else if (!lhsIsZero && rhsIsZero) {
1734 exprTree = lhs;
1735 } else {
1736 return failure();
1737 }
1738
1739 // We now explore the expression tree for multiplications of subtractions/signal values.
1740 std::optional<SourceRef> signalRef = std::nullopt;
1741 DenseSet<Value> signalVals;
1742 SmallVector<DynamicAPInt> consts;
1743 SmallVector<Value> frontier {exprTree};
1744 while (!frontier.empty()) {
1745 Value v = frontier.back();
1746 frontier.pop_back();
1747 Operation *op = v.getDefiningOp();
1748
1749 FeltConstantOp c;
1750 Value signalVal;
1751 auto handleRefValue = [this, &signalRef, &signalVal, &signalVals]() {
1752 SourceRefLatticeValue refSet = getSourceRefState(signalVal);
1753 if (!refSet.isScalar() || !refSet.isSingleValue()) {
1754 return failure();
1755 }
1756 SourceRef r = refSet.getSingleValue();
1757 if (signalRef.has_value() && signalRef.value() != r) {
1758 return failure();
1759 } else if (!signalRef.has_value()) {
1760 signalRef = r;
1761 }
1762 signalVals.insert(signalVal);
1763 return success();
1764 };
1765
1766 auto subPattern = m_CommutativeOp<SubFeltOp>(m_RefValue(&signalVal), m_Constant(&c));
1767 if (op && matchPattern(op, subPattern)) {
1768 if (failed(handleRefValue())) {
1769 return failure();
1770 }
1771 auto constInt = APSInt(c.getValue());
1772 consts.push_back(field.get().reduce(constInt));
1773 continue;
1774 } else if (m_RefValue(&signalVal).match(v)) {
1775 if (failed(handleRefValue())) {
1776 return failure();
1777 }
1778 consts.push_back(field.get().zero());
1779 continue;
1780 }
1781
1782 Value a, b;
1783 auto mulPattern = m_CommutativeOp<MulFeltOp>(matchers::m_Any(&a), matchers::m_Any(&b));
1784 if (op && matchPattern(op, mulPattern)) {
1785 frontier.push_back(a);
1786 frontier.push_back(b);
1787 continue;
1788 }
1789
1790 return failure();
1791 }
1792
1793 // Now, we aggregate the Interval. If we have sparse values (e.g., 0, 2, 4),
1794 // we will create a larger range of [0, 4], since we don't support multiple intervals.
1795 std::sort(consts.begin(), consts.end());
1796 Interval iv = UnreducedInterval(consts.front(), consts.back()).reduce(field.get());
1797 return std::make_pair(std::move(signalVals), iv);
1798}
1799
1800/* StructIntervals */
1801
1803 mlir::DataFlowSolver &solver, mlir::AnalysisManager &am, const IntervalAnalysisContext &ctx
1804) {
1805 SymbolTableCollection tables;
1806
1807 auto computeIntervalsImpl =
1808 [&solver, &am, &ctx, &tables, this](
1809 FuncDefOp fn, llvm::MapVector<SourceRef, Interval> &memberRanges,
1810 llvm::MapVector<SourceRef, UnreducedInterval> &memberUnreducedRanges,
1811 llvm::SetVector<ExpressionValue> & /*solverConstraints*/
1812 ) {
1813 auto setUnreducedRange =
1814 [&memberUnreducedRanges](const SourceRef &ref, const UnreducedInterval &interval) {
1815 memberUnreducedRanges.erase(ref);
1816 memberUnreducedRanges.insert({ref, interval});
1817 };
1818 // Since every lattice value does not contain every value, we will traverse
1819 // the function backwards (from most up-to-date to least-up-to-date lattices)
1820 // searching for the source refs. Once a source ref is found, we remove it
1821 // from the search set.
1822
1823 SourceRefSet searchSet;
1824 for (const auto &ref : SourceRef::getAllSourceRefs(structDef, fn)) {
1825 // We only want to compute intervals for field elements and not composite types.
1826 if (!ref.isScalar()) {
1827 continue;
1828 }
1829 searchSet.insert(ref);
1830 }
1831 SourceRefSet functionRefs = searchSet;
1832
1833 auto mergeInterval = [&memberRanges, &memberUnreducedRanges](
1834 const SourceRef &ref, const Interval &interval,
1835 std::optional<UnreducedInterval> unreducedInterval = std::nullopt
1836 ) {
1837 auto *existing = memberRanges.find(ref);
1838 if (existing != memberRanges.end()) {
1839 Interval mergedInterval = existing->second.intersect(interval);
1840 bool intervalChanged = mergedInterval != existing->second;
1841 existing->second = mergedInterval;
1842
1843 if (unreducedInterval.has_value()) {
1844 auto *existingUnreduced = memberUnreducedRanges.find(ref);
1845 if (existingUnreduced != memberUnreducedRanges.end()) {
1846 existingUnreduced->second = existingUnreduced->second.intersect(*unreducedInterval);
1847 } else {
1848 memberUnreducedRanges.insert({ref, *unreducedInterval});
1849 }
1850 } else if (intervalChanged) {
1851 memberUnreducedRanges.erase(ref);
1852 }
1853 return;
1854 }
1855
1856 memberRanges[ref] = interval;
1857 if (unreducedInterval.has_value()) {
1858 memberUnreducedRanges.insert({ref, *unreducedInterval});
1859 }
1860 };
1861
1862 // Iterate over arguments
1863 for (BlockArgument arg : fn.getArguments()) {
1864 SourceRef ref {arg};
1865 if (searchSet.erase(ref)) {
1866 const IntervalAnalysisLattice *lattice = solver.lookupState<IntervalAnalysisLattice>(arg);
1867 // If we never referenced this argument, use a default value
1868 ExpressionValue expr = lattice->getValue().getScalarValue();
1869 if (!expr.getExpr()) {
1870 expr = expr.withInterval(Interval::Entire(ctx.getField()));
1871 if (ctx.doTrackUnreducedIntervals()) {
1872 expr = expr.withUnreducedInterval(expr.getInterval().firstUnreduced());
1873 }
1874 }
1875 memberRanges[ref] = expr.getInterval();
1876 if (expr.hasUnreducedInterval()) {
1877 setUnreducedRange(ref, expr.getUnreducedInterval());
1878 }
1879 assert(memberRanges[ref].getField() == ctx.getField() && "bad interval defaults");
1880 }
1881 }
1882
1883 // Aggregate all read intervals for a ref. A single ref may be read at multiple program
1884 // points with different precision, so picking an arbitrary lattice from the DenseSet is
1885 // nondeterministic. Joining preserves the overapproximation regardless of iteration order.
1886 for (const auto &[ref, lattices] : ctx.intervalDFA->getReadResults()) {
1887 if (!lattices.empty() && searchSet.erase(ref)) {
1888 Interval joinedInterval = Interval::Empty(ctx.getField());
1889 std::optional<UnreducedInterval> joinedUnreduced = std::nullopt;
1890 bool sawFirst = false;
1891 for (const IntervalAnalysisLattice *lattice : lattices) {
1892 const ExpressionValue &expr = lattice->getValue().getScalarValue();
1893 joinedInterval = joinedInterval.join(expr.getInterval());
1894 if (!sawFirst) {
1895 joinedUnreduced = expr.getOptionalUnreducedInterval();
1896 sawFirst = true;
1897 } else {
1898 joinedUnreduced =
1899 mergeUnreducedIntervals(joinedUnreduced, expr.getOptionalUnreducedInterval());
1900 }
1901 }
1902 memberRanges[ref] = joinedInterval;
1903 if (joinedUnreduced.has_value()) {
1904 setUnreducedRange(ref, *joinedUnreduced);
1905 }
1906 assert(memberRanges[ref].getField() == ctx.getField() && "bad interval defaults");
1907 }
1908 }
1909
1910 for (const auto &[ref, val] : ctx.intervalDFA->getWriteResults()) {
1911 if (searchSet.erase(ref)) {
1912 memberRanges[ref] = val.getInterval();
1913 if (val.hasUnreducedInterval()) {
1914 setUnreducedRange(ref, val.getUnreducedInterval());
1915 }
1916 assert(memberRanges[ref].getField() == ctx.getField() && "bad interval defaults");
1917 }
1918 }
1919
1920 // Child constrain calls refine parent-visible storage, but only after the callee
1921 // summary is translated through the call operands. If translation cannot prove
1922 // which parent ref owns a child interval, the local overapproximation remains.
1923 if (fn.isStructConstrain()) {
1924 auto mergeChildConstrainIntervals = [&](CallOp fnCall) {
1925 if (!dataflow::isOperationLive(solver, fnCall.getOperation())) {
1926 return;
1927 }
1928
1929 auto res = resolveCallableSilently<FuncDefOp>(tables, fnCall);
1930 if (failed(res)) {
1931 return;
1932 }
1933
1934 FuncDefOp calledFn = res->get();
1935 if (!calledFn.isStructConstrain()) {
1936 return;
1937 }
1938
1939 auto calledStruct = calledFn->getParentOfType<StructDefOp>();
1940 if (calledStruct == structDef) {
1941 return;
1942 }
1943
1944 auto &childAnalysis = am.getChildAnalysis<StructIntervalAnalysis>(calledStruct);
1945 if (childAnalysis.inProgress(ctx)) {
1946 return;
1947 }
1948 if (!childAnalysis.constructed(ctx)) {
1949 ensure(
1950 succeeded(childAnalysis.runAnalysis(solver, am, ctx)),
1951 "could not construct interval analysis for child struct"
1952 );
1953 }
1954
1955 // Translate callee argument refs into parent refs and capture scalar call-site intervals
1956 // that can refine direct equality groups inside the child constrain function.
1957 SourceRefRemappings identityTranslations;
1958 llvm::MapVector<SourceRef, Interval> callOperandIntervals;
1959 for (unsigned i = 0; i < calledFn.getNumArguments(); i++) {
1960 SourceRef prefix(calledFn.getArgument(i));
1961 Value operand = fnCall.getOperand(i);
1962 std::optional<SourceRefLatticeValue> identityVal =
1963 getIdentitySourceRefState(solver, operand);
1964 if (identityVal.has_value()) {
1965 identityTranslations.push_back({prefix, *identityVal});
1966 }
1967
1968 if (!llvm::isa<ArrayType, StructType, pod::PodType>(operand.getType())) {
1969 const IntervalAnalysisLattice *lattice =
1970 solver.lookupState<IntervalAnalysisLattice>(operand);
1971 if (lattice != nullptr) {
1972 const ExpressionValue &expr = lattice->getValue().getScalarValue();
1973 callOperandIntervals[prefix] = expr.getInterval();
1974 }
1975 }
1976 }
1977
1978 const StructIntervals &childIntervals = childAnalysis.getResult(ctx);
1979 const auto &constrainIntervals = childIntervals.getConstrainIntervals();
1980 const auto &constrainUnreducedIntervals = childIntervals.getConstrainUnreducedIntervals();
1981 for (const auto &[childRef, childInterval] : constrainIntervals) {
1982 auto translatedRefs = translateRef(childRef, identityTranslations);
1983 if (failed(translatedRefs)) {
1984 continue;
1985 }
1986
1987 std::optional<UnreducedInterval> childUnreduced = std::nullopt;
1988 if (const auto *childUnreducedIt = constrainUnreducedIntervals.find(childRef);
1989 childUnreducedIt != constrainUnreducedIntervals.end()) {
1990 childUnreduced = childUnreducedIt->second;
1991 }
1992
1993 SourceRefSet uniqueTranslatedRefs;
1994 for (const SourceRef &translatedRef : *translatedRefs) {
1995 uniqueTranslatedRefs.insert(translatedRef);
1996 }
1997 if (uniqueTranslatedRefs.size() != 1) {
1998 continue;
1999 }
2000
2001 const SourceRef &translatedRef = *uniqueTranslatedRefs.begin();
2002 if (functionRefs.contains(translatedRef)) {
2003 mergeInterval(translatedRef, childInterval, childUnreduced);
2004 searchSet.erase(translatedRef);
2005 }
2006 }
2007
2008 // Direct equalities in the child can combine child summaries, call operand intervals, and
2009 // existing parent intervals before merging back into each translated parent ref.
2010 llvm::EquivalenceClasses<SourceRef> directEqRefs =
2011 collectDirectEqualityRefs(solver, calledFn);
2012 for (auto leaderIt = directEqRefs.begin(); leaderIt != directEqRefs.end(); ++leaderIt) {
2013 if (!leaderIt->isLeader()) {
2014 continue;
2015 }
2016
2017 llvm::MapVector<SourceRef, Interval> translatedEqRefs;
2018 Interval contextualInterval = Interval::Entire(ctx.getField());
2019 bool hasInterval = false;
2020 bool ambiguousTranslation = false;
2021
2022 for (auto memberIt = directEqRefs.member_begin(leaderIt);
2023 memberIt != directEqRefs.member_end(); ++memberIt) {
2024 Interval memberInterval = Interval::Entire(ctx.getField());
2025 if (const auto *childIntervalIt = constrainIntervals.find(*memberIt);
2026 childIntervalIt != constrainIntervals.end()) {
2027 memberInterval = memberInterval.intersect(childIntervalIt->second);
2028 }
2029 if (auto *callOperandIt = callOperandIntervals.find(*memberIt);
2030 callOperandIt != callOperandIntervals.end()) {
2031 memberInterval = memberInterval.intersect(callOperandIt->second);
2032 contextualInterval = contextualInterval.intersect(memberInterval);
2033 hasInterval = true;
2034 }
2035
2036 auto translatedRefs = translateRef(*memberIt, identityTranslations);
2037 if (failed(translatedRefs)) {
2038 continue;
2039 }
2040
2041 SourceRefSet uniqueTranslatedRefs;
2042 for (const SourceRef &translatedRef : *translatedRefs) {
2043 uniqueTranslatedRefs.insert(translatedRef);
2044 }
2045 if (uniqueTranslatedRefs.size() != 1) {
2046 ambiguousTranslation = true;
2047 break;
2048 }
2049
2050 const SourceRef &translatedRef = *uniqueTranslatedRefs.begin();
2051 if (!functionRefs.contains(translatedRef)) {
2052 continue;
2053 }
2054
2055 if (auto *parentIntervalIt = memberRanges.find(translatedRef);
2056 parentIntervalIt != memberRanges.end()) {
2057 memberInterval = memberInterval.intersect(parentIntervalIt->second);
2058 }
2059
2060 translatedEqRefs[translatedRef] = memberInterval;
2061 contextualInterval = contextualInterval.intersect(memberInterval);
2062 hasInterval = true;
2063 }
2064
2065 if (ambiguousTranslation || !hasInterval || translatedEqRefs.empty()) {
2066 continue;
2067 }
2068
2069 for (const auto &[translatedRef, _] : translatedEqRefs) {
2070 mergeInterval(translatedRef, contextualInterval);
2071 searchSet.erase(translatedRef);
2072 }
2073 }
2074 };
2075
2076 fn.walk(mergeChildConstrainIntervals);
2077 }
2078
2079 // For all unfound refs, default to the entire range.
2080 for (const auto &ref : searchSet) {
2081 memberRanges[ref] = Interval::Entire(ctx.getField());
2082 if (ctx.doTrackUnreducedIntervals()) {
2083 setUnreducedRange(ref, memberRanges[ref].firstUnreduced());
2084 }
2085 }
2086
2087 // Sort the outputs since we assembled things out of order.
2088 //
2089 // `llvm::MapVector` maintains an internal key -> index map. Sorting it in
2090 // place corrupts lookup semantics because the backing vector is reordered
2091 // without rebuilding that map. Reinsert into a fresh MapVector instead.
2092 llvm::SmallVector<std::pair<SourceRef, Interval>> sortedRanges;
2093 sortedRanges.reserve(memberRanges.size());
2094 for (const auto &[ref, interval] : memberRanges) {
2095 sortedRanges.emplace_back(ref, interval);
2096 }
2097 llvm::sort(sortedRanges, [](const auto &a, const auto &b) { return a.first < b.first; });
2098 llvm::SmallVector<std::pair<SourceRef, UnreducedInterval>> sortedUnreducedRanges;
2099 sortedUnreducedRanges.reserve(memberUnreducedRanges.size());
2100 for (const auto &[ref, interval] : memberUnreducedRanges) {
2101 sortedUnreducedRanges.emplace_back(ref, interval);
2102 }
2103 llvm::sort(sortedUnreducedRanges, [](const auto &a, const auto &b) {
2104 return a.first < b.first;
2105 });
2106 memberRanges.clear();
2107 memberUnreducedRanges.clear();
2108 for (auto &[ref, interval] : sortedRanges) {
2109 memberRanges[ref] = interval;
2110 }
2111 for (auto &[ref, interval] : sortedUnreducedRanges) {
2112 memberUnreducedRanges.insert({ref, interval});
2113 }
2114 };
2115
2116 if (auto computeFn = structDef.getComputeFuncOp()) {
2117 computeIntervalsImpl(
2118 computeFn, computeMemberRanges, computeMemberUnreducedRanges, computeSolverConstraints
2119 );
2120 }
2121 if (auto constrainFn = structDef.getConstrainFuncOp()) {
2122 computeIntervalsImpl(
2123 constrainFn, constrainMemberRanges, constrainMemberUnreducedRanges,
2124 constrainSolverConstraints
2125 );
2126 }
2127
2128 return success();
2129}
2130
2132 mlir::raw_ostream &os, bool withConstraints, bool printCompute, bool printUnreduced
2133) const {
2134 auto writeIntervals =
2135 [&os, &withConstraints, &printUnreduced](
2136 const char *fnName, const llvm::MapVector<SourceRef, Interval> &memberRanges,
2137 const llvm::MapVector<SourceRef, UnreducedInterval> &memberUnreducedRanges,
2138 const llvm::SetVector<ExpressionValue> &solverConstraints, bool printName
2139 ) {
2140 int indent = 4;
2141 if (printName) {
2142 os << '\n';
2143 os.indent(indent) << fnName << " {";
2144 indent += 4;
2145 }
2146
2147 if (memberRanges.empty()) {
2148 os << "}\n";
2149 return;
2150 }
2151
2152 for (const auto &[ref, interval] : memberRanges) {
2153 os << '\n';
2154 os.indent(indent) << ref << " in " << interval;
2155 if (printUnreduced) {
2156 const auto *unreducedIt = memberUnreducedRanges.find(ref);
2157 if (unreducedIt != memberUnreducedRanges.end()) {
2158 os << " ( " << unreducedIt->second << " )";
2159 }
2160 }
2161 }
2162
2163 if (withConstraints) {
2164 os << "\n\n";
2165 os.indent(indent) << "Solver Constraints { ";
2166 if (solverConstraints.empty()) {
2167 os << "}\n";
2168 } else {
2169 for (const auto &e : solverConstraints) {
2170 os << '\n';
2171 os.indent(indent + 4);
2172 e.getExpr()->print(os);
2173 }
2174 os << '\n';
2175 os.indent(indent) << '}';
2176 }
2177 }
2178
2179 if (printName) {
2180 os << '\n';
2181 os.indent(indent - 4) << '}';
2182 }
2183 };
2184
2185 os << "StructIntervals { ";
2186 if (constrainMemberRanges.empty() && (!printCompute || computeMemberRanges.empty())) {
2187 os << "}\n";
2188 return;
2189 }
2190
2191 if (printCompute) {
2192 writeIntervals(
2193 FUNC_NAME_COMPUTE, computeMemberRanges, computeMemberUnreducedRanges,
2194 computeSolverConstraints, printCompute
2195 );
2196 }
2197 writeIntervals(
2198 FUNC_NAME_CONSTRAIN, constrainMemberRanges, constrainMemberUnreducedRanges,
2199 constrainSolverConstraints, printCompute
2200 );
2201
2202 os << "\n}\n";
2203}
2204
2205} // namespace llzk
Tracks a solver expression and an interval range for that expression.
ExpressionValue withUnreducedInterval(const UnreducedInterval &newUnreducedInterval) const
ExpressionValue withExpression(const llvm::SMTExprRef &newExpr) const
Return the current expression with a new SMT expression.
const Interval & getInterval() const
const std::optional< UnreducedInterval > & getOptionalUnreducedInterval() const
ExpressionValue withOptionalUnreducedInterval(std::optional< UnreducedInterval > newUnreducedInterval) const
ExpressionValue withInterval(const Interval &newInterval) const
Return the current expression with a new interval.
void print(mlir::raw_ostream &os) const
bool operator==(const ExpressionValue &rhs) const
llvm::SMTExprRef getExpr() const
bool isBoolSort(const llvm::SMTSolverRef &solver) const
bool hasUnreducedInterval() const
const Field & getField() const
const UnreducedInterval & getUnreducedInterval() const
Information about the prime finite field used for the interval analysis.
Definition Field.h:36
llvm::DynamicAPInt zero() const
Returns 0 at the bitwidth of the field.
Definition Field.h:81
llvm::DynamicAPInt prime() const
For the prime field p, returns p.
Definition Field.h:72
llvm::DynamicAPInt one() const
Returns 1 at the bitwidth of the field.
Definition Field.h:84
llvm::DynamicAPInt inv(const llvm::DynamicAPInt &i) const
Returns the multiplicative inverse of i in prime field p.
unsigned bitWidth() const
Definition Field.h:107
llvm::SMTExprRef createSymbol(const llvm::SMTSolverRef &solver, const char *name) const
Create a SMT solver symbol with the current field's bitwidth.
Definition Field.h:112
llvm::DynamicAPInt maxVal() const
Returns p - 1, which is the max value possible in a prime field described by p.
Definition Field.h:87
const LatticeValue & getValue() const
mlir::ChangeResult setValue(const LatticeValue &val)
IntervalAnalysisLatticeValue LatticeValue
mlir::ChangeResult meet(const AbstractSparseLattice &other) override
void print(mlir::raw_ostream &os) const override
mlir::ChangeResult join(const AbstractSparseLattice &other) override
mlir::ChangeResult addSolverConstraint(const ExpressionValue &e)
mlir::LogicalResult visitOperation(mlir::Operation *op, mlir::ArrayRef< const Lattice * > operands, mlir::ArrayRef< Lattice * > results) override
Visit an operation with the lattices of its operands.
llvm::SMTExprRef getOrCreateSymbol(const SourceRef &r)
Either return the existing SMT expression that corresponds to the SourceRef, or create one.
const llvm::DenseMap< SourceRef, ExpressionValue > & getWriteResults() const
const llvm::DenseMap< SourceRef, llvm::DenseSet< Lattice * > > & getReadResults() const
Intervals over a finite field.
Definition Intervals.h:206
bool isEmpty() const
Definition Intervals.h:314
static Interval True(const Field &f)
Definition Intervals.h:225
llvm::DynamicAPInt rhs() const
Definition Intervals.h:339
Interval intersect(const Interval &rhs) const
Intersect.
UnreducedInterval toUnreduced() const
Convert to an UnreducedInterval.
static Interval Boolean(const Field &f)
Definition Intervals.h:227
UnreducedInterval firstUnreduced() const
Get the first side of the interval for TypeF intervals, otherwise just get the full interval as an Un...
static Interval Entire(const Field &f)
Definition Intervals.h:229
bool isDegenerate() const
Definition Intervals.h:316
bool isNotEmpty() const
Definition Intervals.h:315
static Interval False(const Field &f)
Definition Intervals.h:223
llvm::DynamicAPInt lhs() const
Definition Intervals.h:338
Interval join(const Interval &rhs) const
Union.
static SourceRefLatticeValue getValueState(mlir::DataFlowSolver &solver, mlir::Value val)
Defines an index into an LLZK object.
Definition SourceRef.h:43
A value at a given point of the SourceRefLattice.
mlir::FailureOr< std::pair< SourceRefLatticeValue, mlir::ChangeResult > > referencePodRecord(mlir::StringAttr recordName) const
Add the given pod recordName to the SourceRefs contained within this value.
const SourceRef & getSingleValue() const
mlir::FailureOr< std::pair< SourceRefLatticeValue, mlir::ChangeResult > > extract(const std::vector< SourceRefIndex > &indices) const
Perform an array.extract or array.read operation, depending on how many indices are provided.
A reference to a "source", which is the base value from which other SSA values are derived.
Definition SourceRef.h:146
mlir::FailureOr< SourceRef > createChild(const SourceRefIndex &r) const
Definition SourceRef.h:357
bool isScalar() const
Definition SourceRef.h:253
std::vector< SourceRefIndex > Path
Definition SourceRef.h:148
static std::vector< SourceRef > getAllSourceRefs(mlir::SymbolTableCollection &tables, mlir::ModuleOp mod, const SourceRef &root)
Produce all possible SourceRefs that are present starting from the given root.
mlir::Type getType() const
const llvm::MapVector< SourceRef, Interval > & getConstrainIntervals() const
const llvm::MapVector< SourceRef, UnreducedInterval > & getConstrainUnreducedIntervals() const
void print(mlir::raw_ostream &os, bool withConstraints=false, bool printCompute=false, bool printUnreduced=false) const
mlir::LogicalResult computeIntervals(mlir::DataFlowSolver &solver, mlir::AnalysisManager &am, const IntervalAnalysisContext &ctx)
An inclusive interval [a, b] where a and b are arbitrary integers not necessarily bound to a given fi...
Definition Intervals.h:26
UnreducedInterval computeLTPart(const UnreducedInterval &rhs) const
Return the part of the interval that is guaranteed to be less than the rhs's max value.
Definition Intervals.cpp:63
UnreducedInterval computeGEPart(const UnreducedInterval &rhs) const
Return the part of the interval that is greater than or equal to the rhs's lower bound.
Definition Intervals.cpp:86
UnreducedInterval computeGTPart(const UnreducedInterval &rhs) const
Return the part of the interval that is greater than the rhs's lower bound.
Definition Intervals.cpp:78
Interval reduce(const Field &field) const
Reduce the interval to an interval in the given field.
Definition Intervals.cpp:23
UnreducedInterval computeLEPart(const UnreducedInterval &rhs) const
Return the part of the interval that is less than or equal to the rhs's upper bound.
Definition Intervals.cpp:71
Helper for converting between linear and multi-dimensional indexing with checks to ensure indices are...
static ArrayIndexGen from(ArrayType)
Construct new ArrayIndexGen. Will assert if hasStaticShape() is false.
std::optional< llvm::SmallVector< mlir::Value > > delinearize(int64_t, mlir::Location, mlir::OpBuilder &) const
::mlir::Type getElementType() const
::llzk::boolean::FeltCmpPredicate getPredicate()
Definition Ops.cpp.inc:873
std::variant< ScalarTy, ArrayTy > & getValue()
IntervalAnalysisLattice * getLatticeElement(mlir::Value value) override
bool isStructConstrain()
Return true iff the function is within a StructDefOp and named FUNC_NAME_CONSTRAIN.
Definition Ops.h.inc:902
bool isOperationLive(DataFlowSolver &solver, Operation *op)
ExpressionValue boolNot(const llvm::SMTSolverRef &solver, const ExpressionValue &val)
ExpressionValue add(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
constexpr char FUNC_NAME_COMPUTE[]
Symbol name for the witness generation (and resp.
Definition Constants.h:16
ExpressionValue sintMod(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
RefValueCapture m_RefValue()
Definition Matchers.h:69
ExpressionValue intersection(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
FailureOr< Interval > signedIntDiv(const Interval &lhs, const Interval &rhs)
Computes signed integer division with possibly non-Degenerate divisors.
std::vector< std::pair< SourceRef, SourceRefLatticeValue > > SourceRefRemappings
ExpressionValue mod(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
ExpressionValue shiftLeft(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
ExpressionValue fallbackUnaryOp(const llvm::SMTSolverRef &solver, Operation *op, const ExpressionValue &val)
constexpr char FUNC_NAME_CONSTRAIN[]
Definition Constants.h:17
Interval signedMod(const Interval &lhs, const Interval &rhs)
Computes signed integer remainder with possibly non-Degenerate divisors.
void ensure(bool condition, const llvm::Twine &errMsg)
ExpressionValue boolXor(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
ExpressionValue cmp(const llvm::SMTSolverRef &solver, CmpOp op, const ExpressionValue &lhs, const ExpressionValue &rhs)
ExpressionValue neg(const llvm::SMTSolverRef &solver, const ExpressionValue &val)
DynamicAPInt toDynamicAPInt(StringRef str)
llvm::SMTExprRef createFieldInverseExpr(const llvm::SMTSolverRef &solver, Operation *op, const ExpressionValue &val, StringRef suffix="")
ExpressionValue sintDiv(const llvm::SMTSolverRef &solver, Operation *op, const ExpressionValue &lhs, const ExpressionValue &rhs)
ExpressionValue boolAnd(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
FailureOr< Interval > unsignedIntDiv(const Interval &lhs, const Interval &rhs)
Computes unsigned integer division with possibly non-Degenerate divisors.
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)
std::string buildStringViaPrint(const T &base, Args &&...args)
Generate a string by calling base.print(llvm::raw_ostream &) on a stream backed by the returned strin...
ExpressionValue boolToFelt(const llvm::SMTSolverRef &solver, const ExpressionValue &expr, unsigned bitwidth)
mlir::FailureOr< SymbolLookupResult< T > > resolveCallableSilently(mlir::SymbolTableCollection &symbolTable, mlir::CallOpInterface call)
Resolve a callable without emitting a diagnostic for missing top-level symbols.
ConstantCapture m_Constant()
Definition Matchers.h:89
std::string buildStringViaInsertionOp(Args &&...args)
Generate a string by using the insertion operator (<<) to append all args to a stream backed by the r...
ExpressionValue bitOr(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
ExpressionValue uintDiv(const llvm::SMTSolverRef &solver, Operation *op, const ExpressionValue &lhs, const ExpressionValue &rhs)
ExpressionValue bitAnd(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
ExpressionValue shiftRight(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
APSInt toAPSInt(const DynamicAPInt &i)
ExpressionValue sub(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
auto m_CommutativeOp(LhsMatcher lhs, RhsMatcher rhs)
Definition Matchers.h:47
ExpressionValue bitXor(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
ExpressionValue notOp(const llvm::SMTSolverRef &solver, const ExpressionValue &val)
FailureOr< Interval > feltDiv(const Interval &lhs, const Interval &rhs)
Computes finite-field division by multiplying the dividend by the multiplicative inverse of the divis...
ExpressionValue boolOr(const llvm::SMTSolverRef &solver, const ExpressionValue &lhs, const ExpressionValue &rhs)
ExpressionValue selectValue(const llvm::SMTSolverRef &solver, const ExpressionValue &cond, const ExpressionValue &trueVal, const ExpressionValue &falseVal)
Parameters and shared objects to pass to child analyses.
const Field & getField() const
IntervalDataFlowAnalysis * intervalDFA