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 auto constantIndex = idxVals.getConstantValue(field.get());
694 if (succeeded(constantIndex)) {
695 indices.emplace_back(*constantIndex);
696 } else {
697 indices.push_back(SourceRefIndex::forArrayDimension(arrayType.getDimSize(i)));
698 }
699 }
700
701 return indices;
702}
703
704mlir::FailureOr<SourceRef> IntervalDataFlowAnalysis::getArrayAccessRef(
705 Operation *baseOp, ArrayAccessOpInterface arrayAccessOp
706) {
707 std::vector<SourceRefIndex> indices = getArrayAccessIndices(baseOp, arrayAccessOp);
708 Value arrayVal = arrayAccessOp.getArrRef();
709 if (auto blockArg = llvm::dyn_cast<BlockArgument>(arrayVal)) {
710 return SourceRef(blockArg, std::move(indices));
711 }
712 if (auto result = llvm::dyn_cast<OpResult>(arrayVal)) {
713 return SourceRef(result, std::move(indices));
714 }
715 return failure();
716}
717
718Interval IntervalDataFlowAnalysis::getRefInterval(const SourceRef &ref) {
719 if (auto it = writeResults.find(ref); it != writeResults.end()) {
720 return it->second.getInterval();
721 }
722
723 if (ref.isConstantInt()) {
724 auto constVal = ref.getConstantValue(field.get());
725 if (succeeded(constVal)) {
726 return Interval::Degenerate(field.get(), *constVal);
727 }
728 }
729
730 if (ref.isRooted() && ref.getPath().empty()) {
731 auto rootVal = ref.getRoot();
732 if (succeeded(rootVal) && !llvm::isa<ArrayType, StructType, pod::PodType>(rootVal->getType())) {
733 const ExpressionValue &rootExpr = getLatticeElement(*rootVal)->getValue().getScalarValue();
734 if (rootExpr.getExpr() != nullptr) {
735 return rootExpr.getInterval();
736 }
737 }
738 }
739
740 return getDefaultIntervalForType(ref.getType());
741}
742
743std::optional<UnreducedInterval>
744IntervalDataFlowAnalysis::getDefaultUnreducedIntervalForType(mlir::Type ty) const {
745 if (!trackUnreducedIntervals) {
746 return std::nullopt;
747 }
748 if (isBooleanType(ty)) {
749 return UnreducedInterval(0, 1);
750 }
751 return UnreducedInterval(field.get().zero(), field.get().maxVal());
752}
753
754std::optional<UnreducedInterval>
755IntervalDataFlowAnalysis::getRefUnreducedInterval(const SourceRef &ref) {
756 if (!trackUnreducedIntervals) {
757 return std::nullopt;
758 }
759
760 if (auto it = writeResults.find(ref); it != writeResults.end()) {
761 return it->second.getOptionalUnreducedInterval();
762 }
763
764 if (ref.isConstantInt()) {
765 auto constVal = ref.getConstantValue(field.get());
766 if (succeeded(constVal)) {
767 return UnreducedInterval(*constVal, *constVal);
768 }
769 }
770
771 if (ref.isRooted() && ref.getPath().empty()) {
772 auto rootVal = ref.getRoot();
773 if (succeeded(rootVal) && !llvm::isa<ArrayType, StructType, pod::PodType>(rootVal->getType())) {
774 const ExpressionValue &rootExpr = getLatticeElement(*rootVal)->getValue().getScalarValue();
775 if (rootExpr.hasUnreducedInterval()) {
776 return rootExpr.getUnreducedInterval();
777 }
778 }
779 }
780
781 return getRefInterval(ref).firstUnreduced();
782}
783
784ExpressionValue IntervalDataFlowAnalysis::getRefValue(const SourceRef &ref, Value val) {
785 if (auto it = writeResults.find(ref); it != writeResults.end()) {
786 return it->second;
787 }
788 return createUnknownValue(val)
789 .withInterval(getRefInterval(ref))
790 .withOptionalUnreducedInterval(getRefUnreducedInterval(ref));
791}
792
793void IntervalDataFlowAnalysis::recordRefWrite(
794 const SourceRef &writtenRef, const ExpressionValue &writeVal, bool mayBeSkipped
795) {
796 auto joinStoredWrite = [this, &writtenRef](
797 const ExpressionValue &old, const ExpressionValue &next
798 ) -> ExpressionValue {
799 Interval combinedWrite = old.getInterval().join(next.getInterval());
800 auto combinedUnreduced = mergeUnreducedIntervals(
801 old.getOptionalUnreducedInterval(), next.getOptionalUnreducedInterval()
802 );
803 if (old.getExpr() != nullptr && next.getExpr() != nullptr &&
804 *old.getExpr() == *next.getExpr()) {
805 return old.withInterval(combinedWrite).withOptionalUnreducedInterval(combinedUnreduced);
806 }
807
808 return ExpressionValue(
809 getOrCreateSymbol(writtenRef), combinedWrite, std::move(combinedUnreduced)
810 );
811 };
812
813 if (auto it = writeResults.find(writtenRef); it != writeResults.end()) {
814 it->second = joinStoredWrite(it->second, writeVal);
815 } else if (mayBeSkipped) {
816 ExpressionValue noWrite(
817 getOrCreateSymbol(writtenRef), getRefInterval(writtenRef),
818 getRefUnreducedInterval(writtenRef)
819 );
820 writeResults[writtenRef] = joinStoredWrite(noWrite, writeVal);
821 } else {
822 writeResults[writtenRef] = writeVal;
823 }
824
825 const ExpressionValue &readerUpdate = mayBeSkipped ? writeResults[writtenRef] : writeVal;
826 for (Lattice *readerLattice : readResults[writtenRef]) {
827 ExpressionValue prior = readerLattice->getValue().getScalarValue();
828 Interval intersection = prior.getInterval().intersect(readerUpdate.getInterval());
829 ExpressionValue newVal = prior.withInterval(intersection);
830 propagateIfChanged(readerLattice, readerLattice->setValue(newVal));
831 }
832}
833
835 Operation *op, ArrayRef<const Lattice *> operands, ArrayRef<Lattice *> results
836) {
837 // We only perform the visitation on operations within functions
838 FuncDefOp fn = op->getParentOfType<FuncDefOp>();
839 if (!fn) {
840 return success();
841 }
842
843 // If there are no operands or results, skip.
844 if (operands.empty() && results.empty()) {
845 return success();
846 }
847
848 // Get the values or defaults from the operand lattices
849 llvm::SmallVector<LatticeValue> operandVals;
850 llvm::SmallVector<std::optional<SourceRef>> operandRefs;
851 auto resolveRefStateValue =
852 [&](Value value, const SourceRefLatticeValue &refSet) -> std::optional<LatticeValue> {
853 ensure(refSet.isScalar(), "should have ruled out array values already");
854
855 if (refSet.getScalarValue().empty()) {
856 // If we can't compute the reference, then there must be some unsupported
857 // op the reference analysis cannot handle. We emit a warning and return
858 // early, since there's no meaningful computation we can do for this op.
859 op->emitWarning()
860 .append(
861 "state of ", value,
862 " is empty; defining operation is unsupported by SourceRef analysis"
863 )
864 .report();
865 return std::nullopt;
866 }
867
868 if (!refSet.isSingleValue()) {
869 Interval joinedInterval = Interval::Empty(field.get());
870 std::optional<UnreducedInterval> joinedUnreduced = std::nullopt;
871 bool sawFirst = false;
872 for (const SourceRef &ref : refSet.getScalarValue()) {
873 joinedInterval = joinedInterval.join(getRefInterval(ref));
874 auto refUnreduced = getRefUnreducedInterval(ref);
875 if (!sawFirst) {
876 joinedUnreduced = refUnreduced;
877 sawFirst = true;
878 } else {
879 joinedUnreduced = mergeUnreducedIntervals(joinedUnreduced, refUnreduced);
880 }
881 }
882 ExpressionValue anyVal = createUnknownValue(value)
883 .withInterval(joinedInterval)
884 .withOptionalUnreducedInterval(joinedUnreduced);
885 return LatticeValue(anyVal);
886 }
887
888 return LatticeValue(getRefValue(refSet.getSingleValue(), value));
889 };
890 for (unsigned opNum = 0; opNum < op->getNumOperands(); ++opNum) {
891 Value val = op->getOperand(opNum);
892 SourceRefLatticeValue refSet = getSourceRefState(val);
893 if (refSet.isSingleValue()) {
894 operandRefs.push_back(refSet.getSingleValue());
895 } else {
896 operandRefs.push_back(std::nullopt);
897 }
898 // First, lookup the operand value after it is initialized
899 auto priorState = operands[opNum]->getValue();
900 if (priorState.getScalarValue().getExpr() != nullptr) {
901 operandVals.push_back(priorState);
902 continue;
903 }
904
905 if (auto readArr = llvm::dyn_cast_if_present<ReadArrayOp>(val.getDefiningOp())) {
906 auto arrayRef = getArrayAccessRef(op, readArr);
907 if (succeeded(arrayRef)) {
908 if (auto it = writeResults.find(*arrayRef); it != writeResults.end()) {
909 operandVals.emplace_back(it->second);
910 Lattice *operandLattice = getLatticeElement(val);
911 (void)operandLattice->setValue(it->second);
912 continue;
913 }
914 }
915 }
916
917 // Else, look up the stored value by `SourceRef`.
918 // We only care about scalar type values here, so ignore aggregate storage values.
919 Type valTy = val.getType();
920 if (llvm::isa<ArrayType, StructType, pod::PodType>(valTy)) {
921 ExpressionValue anyVal(field.get(), createSymbol(valTy, buildStringViaPrint(val).c_str()));
922 operandVals.emplace_back(anyVal);
923 continue;
924 }
925
926 auto resolvedValue = resolveRefStateValue(val, refSet);
927 if (!resolvedValue.has_value()) {
928 // We still return success so we can return overapproximated and partial
929 // results to the user.
930 return success();
931 }
932 operandVals.push_back(*resolvedValue);
933
934 // Since we initialized a value that was not found in the before lattice,
935 // update that value in the lattice so we can find it later, but we don't
936 // need to propagate the changes, since we already have what we need.
937 Lattice *operandLattice = getLatticeElement(val);
938 (void)operandLattice->setValue(operandVals[opNum]);
939 }
940
941 if (isReadOp(op) && op->getNumResults() == 1) {
942 Value resultVal = op->getResult(0);
943 if (!llvm::isa<ArrayType, StructType, pod::PodType>(resultVal.getType())) {
944 auto resolvedValue = resolveRefStateValue(resultVal, getSourceRefState(resultVal));
945 if (resolvedValue.has_value()) {
946 propagateIfChanged(results[0], results[0]->setValue(*resolvedValue));
947 }
948 }
949 return success();
950 }
951
952 // Now, the way we update is dependent on the type of the operation.
953 if (isConstOp(op)) {
954 llvm::DynamicAPInt constVal = getConst(op);
955 llvm::SMTExprRef expr;
956 if (isBoolConstOp(op)) {
957 expr = createConstBoolExpr(constVal != 0);
958 } else {
959 expr = createConstBitvectorExpr(constVal);
960 }
961
962 ExpressionValue latticeVal(field.get(), expr, constVal);
963 if (trackUnreducedIntervals) {
964 latticeVal = latticeVal.withUnreducedInterval(UnreducedInterval(constVal, constVal));
965 }
966 propagateIfChanged(results[0], results[0]->setValue(latticeVal));
967 } else if (isArithmeticOp(op)) {
968 ExpressionValue result;
969 if (operands.size() == 2) {
970 result = performBinaryArithmetic(op, operandVals[0], operandVals[1]);
971 } else {
972 result = performUnaryArithmetic(op, operandVals[0]);
973 }
974
975 // Also intersect with prior interval, if it's initialized
976 const ExpressionValue &prior = results[0]->getValue().getScalarValue();
977 if (prior.getExpr()) {
978 result = refineReducedInterval(result, result.getInterval().intersect(prior.getInterval()));
979 }
980 propagateIfChanged(results[0], results[0]->setValue(result));
981 } else if (auto selectOp = llvm::dyn_cast<arith::SelectOp>(op)) {
983 smtSolver, operandVals[0].getScalarValue(), operandVals[1].getScalarValue(),
984 operandVals[2].getScalarValue()
985 );
986 const ExpressionValue &prior = results[0]->getValue().getScalarValue();
987 if (prior.getExpr()) {
988 result = refineReducedInterval(result, result.getInterval().intersect(prior.getInterval()));
989 }
990 propagateIfChanged(results[0], results[0]->setValue(result));
991 } else if (EmitEqualityOp emitEq = llvm::dyn_cast<EmitEqualityOp>(op)) {
992 Value lhsVal = emitEq.getLhs(), rhsVal = emitEq.getRhs();
993 ExpressionValue lhsExpr = operandVals[0].getScalarValue();
994 ExpressionValue rhsExpr = operandVals[1].getScalarValue();
995
996 // Special handling for generalized (s - c0) * (s - c1) * ... * (s - cN) = 0 patterns.
997 // These patterns enforce that s is one of c0, ..., cN.
998 auto res = getGeneralizedDecompInterval(op, lhsVal, rhsVal);
999 if (succeeded(res)) {
1000 for (Value signalVal : res->first) {
1001 applyInterval(emitEq, signalVal, res->second);
1002 }
1003 }
1004
1005 ExpressionValue constraint = intersection(smtSolver, lhsExpr, rhsExpr);
1006 // Update the LHS and RHS to the same value, but restricted intervals
1007 // based on the constraints.
1008 const Interval &constrainInterval = constraint.getInterval();
1009 applyInterval(emitEq, lhsVal, constrainInterval);
1010 applyInterval(emitEq, rhsVal, constrainInterval);
1011 } else if (auto assertOp = llvm::dyn_cast<AssertOp>(op)) {
1012 // assert enforces that the operand is true. So we apply an interval of [1, 1]
1013 // to the operand.
1014 Value cond = assertOp.getCondition();
1015 applyInterval(assertOp, cond, Interval::True(field.get()));
1016 // Also add the solver constraint that the expression must be true.
1017 auto assertExpr = operandVals[0].getScalarValue();
1018 // No need to propagate the constraint
1019 (void)getLatticeElement(cond)->addSolverConstraint(assertExpr);
1020 } else if (auto writePod = llvm::dyn_cast<pod::WritePodOp>(op)) {
1021 const bool maySkipWrite = isInMaybeSkippedScfRegion(op);
1022 SourceRefLatticeValue podRefs = getSourceRefState(writePod.getPodRef());
1023 if (podRefs.isScalar()) {
1024 auto recordRefsRes = podRefs.referencePodRecord(writePod.getRecordNameAttr());
1025 ensure(succeeded(recordRefsRes), "could not create SourceRef child for pod write");
1026 SourceRefLatticeValue recordRefs = recordRefsRes->first;
1027 Type valueTy = writePod.getValue().getType();
1028 if (!llvm::isa<ArrayType, StructType, pod::PodType>(valueTy)) {
1029 ExpressionValue writeVal = operandVals[1].getScalarValue();
1030 for (const SourceRef &recordRef : recordRefs.getScalarValue()) {
1031 recordRefWrite(recordRef, writeVal, maySkipWrite);
1032 }
1033 } else if (operandRefs[1].has_value()) {
1034 llvm::SmallVector<std::pair<SourceRef, ExpressionValue>> remappedWrites;
1035 for (const SourceRef &recordRef : recordRefs.getScalarValue()) {
1036 for (const auto &[writtenRef, writtenVal] : writeResults) {
1037 if (writtenRef.isValidPrefix(*operandRefs[1])) {
1038 auto translated = writtenRef.translate(*operandRefs[1], recordRef);
1039 ensure(succeeded(translated), "could not translate aggregate pod write");
1040 remappedWrites.emplace_back(*translated, writtenVal);
1041 }
1042 }
1043 }
1044 for (const auto &[translatedRef, translatedVal] : remappedWrites) {
1045 recordRefWrite(translatedRef, translatedVal, maySkipWrite);
1046 }
1047 }
1048 }
1049 } else if (auto writem = llvm::dyn_cast<MemberWriteOp>(op)) {
1050 const bool maySkipWrite = isInMaybeSkippedScfRegion(op);
1051 // Update values stored in a member
1052 ExpressionValue writeVal = operandVals[1].getScalarValue();
1053 auto cmp = writem.getComponent();
1054 // We also need to update the interval on the assigned symbol
1055 SourceRefLatticeValue refSet = getSourceRefState(cmp);
1056 if (refSet.isSingleValue()) {
1057 auto memberDefRes = writem.getMemberDefOp(tables);
1058 if (succeeded(memberDefRes)) {
1059 SourceRefIndex idx(memberDefRes.value());
1060 auto memberRefRes = refSet.getSingleValue().createChild(idx);
1061 ensure(succeeded(memberRefRes), "could not create SourceRef child for member write");
1062 const SourceRef &memberRef = *memberRefRes;
1063 Type memberTy = writem.getVal().getType();
1064 if (!llvm::isa<ArrayType, StructType, pod::PodType>(memberTy)) {
1065 // Simple scalar update
1066 recordRefWrite(memberRef, writeVal, maySkipWrite);
1067 } else {
1068 // Map the intervals of aggregates to the written member
1069 std::optional<SourceRef> rhsPrefix;
1070 if (operandRefs[1].has_value() && operandRefs[1]->isRooted()) {
1071 rhsPrefix = operandRefs[1];
1072 } else if (auto blockArg = llvm::dyn_cast<BlockArgument>(writem.getVal())) {
1073 rhsPrefix = SourceRef(blockArg);
1074 } else if (auto result = llvm::dyn_cast<OpResult>(writem.getVal())) {
1075 rhsPrefix = SourceRef(result);
1076 }
1077
1078 if (rhsPrefix.has_value()) {
1079 llvm::SmallVector<std::pair<SourceRef, ExpressionValue>> remappedWrites;
1080 for (const auto &[writtenRef, writtenVal] : writeResults) {
1081 if (!writtenRef.isValidPrefix(*rhsPrefix)) {
1082 continue;
1083 }
1084
1085 auto translatedRef = writtenRef.translate(*rhsPrefix, memberRef);
1086 ensure(succeeded(translatedRef), "could not translate composite member write");
1087 remappedWrites.emplace_back(*translatedRef, writtenVal);
1088 }
1089
1090 for (const auto &[translatedRef, translatedVal] : remappedWrites) {
1091 recordRefWrite(translatedRef, translatedVal, maySkipWrite);
1092 }
1093 }
1094 }
1095 }
1096 }
1097 } else if (auto writeArr = llvm::dyn_cast<WriteArrayOp>(op)) {
1098 const bool maySkipWrite = isInMaybeSkippedScfRegion(op);
1099 ExpressionValue writeVal = operandVals.back().getScalarValue();
1100 auto arrayRef = getArrayAccessRef(op, writeArr);
1101 if (succeeded(arrayRef)) {
1102 recordRefWrite(*arrayRef, writeVal, maySkipWrite);
1103 }
1104
1105 SourceRefLatticeValue arrayVals = getSourceRefState(writeArr.getArrRef());
1106 if (arrayVals.isScalar()) {
1107 std::vector<SourceRefIndex> indices = getArrayAccessIndices(op, writeArr);
1108 auto targetRefsRes = arrayVals.extract(indices);
1109 ensure(succeeded(targetRefsRes), "could not create SourceRef child for array write");
1110 auto [targetRefs, _] = *targetRefsRes;
1111 ensure(targetRefs.isScalar(), "array write must resolve to scalar references");
1112 for (const SourceRef &ref : targetRefs.getScalarValue()) {
1113 recordRefWrite(ref, writeVal, maySkipWrite);
1114 }
1115 }
1116 } else if (auto createArray = llvm::dyn_cast<CreateArrayOp>(op)) {
1117 const auto &elements = createArray.getElements();
1118 ArrayType arrayTy = createArray.getType();
1119 Type elemTy = arrayTy.getElementType();
1120
1121 if (!elements.empty() && !llvm::isa<ArrayType, StructType, pod::PodType>(elemTy)) {
1122 ensure(arrayTy.hasStaticShape(), "array.new with explicit elements must have static shape");
1123 ensure(
1124 std::cmp_equal(elements.size(), arrayTy.getNumElements()),
1125 "array.new explicit initializer length must match array shape"
1126 );
1127
1129 auto arrayRes = llvm::cast<OpResult>(createArray->getResult(0));
1130 for (unsigned i = 0; i < elements.size(); ++i) {
1131 auto maybeIndices = indexGen.delinearize(i, op->getContext());
1132 ensure(maybeIndices.has_value(), "could not delinearize array.new element index");
1133
1134 SourceRef::Path path;
1135 path.reserve(maybeIndices->size());
1136 for (Attribute attr : *maybeIndices) {
1137 auto idxAttr = llvm::dyn_cast<IntegerAttr>(attr);
1138 ensure(idxAttr != nullptr, "array.new delinearize should produce integer attributes");
1139 path.emplace_back(integerAttrToDynamicAPInt(idxAttr));
1140 }
1141
1142 recordRefWrite(SourceRef(arrayRes, std::move(path)), operandVals[i].getScalarValue());
1143 }
1144 } else if (!elements.empty()) {
1145 ensure(arrayTy.hasStaticShape(), "aggregate array.new initializer requires static shape");
1147 SourceRef arrayRoot(llvm::cast<OpResult>(createArray.getResult()));
1148 llvm::SmallVector<std::pair<SourceRef, ExpressionValue>> remappedWrites;
1149 for (auto [i, element] : llvm::enumerate(elements)) {
1150 SourceRefLatticeValue elementRefs = getSourceRefState(element);
1151 if (!elementRefs.isSingleValue()) {
1152 continue;
1153 }
1154 auto maybeIndices = indexGen.delinearize(checkedCast<int64_t>(i), op->getContext());
1155 ensure(maybeIndices.has_value(), "could not delinearize aggregate array.new index");
1156 SourceRef elementTarget = arrayRoot;
1157 for (Attribute attr : *maybeIndices) {
1158 auto child = elementTarget.createChild(
1159 SourceRefIndex(integerAttrToDynamicAPInt(llvm::cast<IntegerAttr>(attr)))
1160 );
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 (
1204 auto intToFelt = dyn_cast<IntToFeltOp>(op);
1205 intToFelt && intToFelt.getOverflow() != cast::OverflowSemantics::ASSERT &&
1206 !intToFelt.getValue().getType().isInteger(1)
1207 ) {
1208 // Non-assert casts can change the value non-monotonically (other than boolean inputs which
1209 // cannot overflow and retain their relation in the cast case below). Forget the operand
1210 // relation, but initialize the result so sparse dataflow visits its users.
1211 propagateIfChanged(results[0], results[0]->setValue(createUnknownValue(intToFelt.getResult())));
1212 } else if (
1213 auto feltToIndex = dyn_cast<FeltToIndexOp>(op);
1214 feltToIndex && feltToIndex.getOverflow() != cast::OverflowSemantics::ASSERT
1215 ) {
1216 // See the IntToFeltOp case above.
1217 propagateIfChanged(results[0], results[0]->setValue(createUnknownValue(op->getResult(0))));
1218 } else if (isa<IntToFeltOp, FeltToIndexOp>(op)) {
1219 // Casts don't modify the intervals, but they do modify the SMT types.
1220 ExpressionValue expr = operandVals[0].getScalarValue();
1221 // We treat all ints and indexes as felts with the exception of comparison
1222 // results, which are bools. So if `expr` is a bool, this cast needs to
1223 // upcast to a felt.
1224 if (expr.isBoolSort(smtSolver)) {
1225 expr = boolToFelt(smtSolver, expr, field.get().bitWidth());
1226 }
1227 propagateIfChanged(results[0], results[0]->setValue(expr));
1228 } else if (auto yieldOp = dyn_cast<scf::YieldOp>(op)) {
1229 Operation *parent = op->getParentOp();
1230 ensure(parent, "yield operation must have parent operation");
1231
1232 // Bind scalar yielded values to the corresponding parent results.
1233 for (unsigned idx = 0; idx < yieldOp.getResults().size(); ++idx) {
1234 Value parentRes = parent->getResult(idx);
1235 Lattice *resLattice = getLatticeElement(parentRes);
1236 // Merge with the existing value, if present (e.g., another branch)
1237 // has possible value that must be merged.
1238 ExpressionValue exprVal = resLattice->getValue().getScalarValue();
1239 ExpressionValue newResVal = operandVals[idx].getScalarValue();
1240 if (auto loopOp = llvm::dyn_cast<LoopLikeOpInterface>(parent)) {
1241 // We overapproximate for loops because we aren't going to try to track trip count.
1242 newResVal = ExpressionValue(createSymbol(parentRes), Interval::Entire(field.get()));
1243 }
1244 if (exprVal.getExpr() != nullptr) {
1245 newResVal =
1246 exprVal.withInterval(exprVal.getInterval().join(newResVal.getInterval()))
1247 .withOptionalUnreducedInterval(mergeUnreducedIntervals(
1249 ));
1250 } else {
1251 newResVal = ExpressionValue(
1252 createSymbol(parentRes), newResVal.getInterval(),
1254 );
1255 }
1256 propagateIfChanged(resLattice, resLattice->setValue(newResVal));
1257 }
1258
1259 // Aggregate intervals are tracked in `writeResults`, so remap writes
1260 // rooted at each yielded aggregate onto the corresponding result.
1261 for (unsigned idx = 0; idx < yieldOp.getNumOperands(); ++idx) {
1262 Value parentRes = parent->getResult(idx);
1263 if (!llvm::isa<ArrayType, StructType, pod::PodType>(parentRes.getType())) {
1264 continue;
1265 }
1266
1267 SourceRefLatticeValue yieldedRefs = getSourceRefState(yieldOp.getOperand(idx));
1268 if (!yieldedRefs.isScalar()) {
1269 continue;
1270 }
1271
1272 SourceRef resultRef(llvm::cast<OpResult>(parentRes));
1273 llvm::SmallVector<std::pair<SourceRef, ExpressionValue>> remappedWrites;
1274 for (const SourceRef &yieldedRef : yieldedRefs.getScalarValue()) {
1275 for (const auto &[writtenRef, writtenVal] : writeResults) {
1276 if (!writtenRef.isValidPrefix(yieldedRef)) {
1277 continue;
1278 }
1279 auto translatedRef = writtenRef.translate(yieldedRef, resultRef);
1280 ensure(succeeded(translatedRef), "could not translate aggregate loop yield");
1281 remappedWrites.emplace_back(*translatedRef, writtenVal);
1282 }
1283 }
1284 for (const auto &[translatedRef, translatedVal] : remappedWrites) {
1285 recordRefWrite(translatedRef, translatedVal);
1286 }
1287 }
1288 } else if (
1289 // We do not need to explicitly handle read ops since they are resolved at the operand value
1290 // step where `SourceRef`s are queries.
1291 !isReadOp(op)
1292 // We do not currently handle return ops as the analysis is currently limited to constrain
1293 // functions, which return no value.
1294 && !isReturnOp(op)
1295 // The analysis ignores definition ops.
1296 && !isDefinitionOp(op)
1297 // We do not need to analyze storage creation directly.
1298 && !llvm::isa<CreateArrayOp, CreateStructOp, pod::NewPodOp, NonDetOp>(op)
1299 ) {
1300 op->emitWarning("unhandled operation, analysis may be incomplete").report();
1301 }
1302
1303 return success();
1304}
1305
1307 auto it = refSymbols.find(r);
1308 if (it != refSymbols.end()) {
1309 return it->second;
1310 }
1311 llvm::SMTExprRef sym = createSymbol(r);
1312 refSymbols[r] = sym;
1313 return sym;
1314}
1315
1316llvm::SMTExprRef IntervalDataFlowAnalysis::createSymbol(mlir::Type ty, const char *name) const {
1317 if (isBooleanType(ty)) {
1318 return smtSolver->mkSymbol(name, smtSolver->getBoolSort());
1319 }
1320 return field.get().createSymbol(smtSolver, name);
1321}
1322
1323llvm::SMTExprRef IntervalDataFlowAnalysis::createSymbol(const SourceRef &r) const {
1324 std::string name = buildStringViaPrint(r);
1325 return createSymbol(r.getType(), name.c_str());
1326}
1327
1328llvm::SMTExprRef IntervalDataFlowAnalysis::createSymbol(Value v) const {
1329 std::string name = buildStringViaPrint(v);
1330 return createSymbol(v.getType(), name.c_str());
1331}
1332
1333llvm::DynamicAPInt IntervalDataFlowAnalysis::getConst(Operation *op) const {
1334 ensure(isConstOp(op), "op is not a const op");
1335
1336 // NOTE: I think clang-format makes these hard to read by default
1337 // clang-format off
1338 llvm::DynamicAPInt fieldConst = TypeSwitch<Operation *, llvm::DynamicAPInt>(op)
1339 .Case<FeltConstantOp>([&](auto feltConst) {
1340 // Preserve felt.const's declared-field meaning before reduction in the analysis field.
1341 // Fieldless literals instead use the raw fallback, as in SourceRef::getConstantValue.
1342 return field.get().reduce(feltConst.getValue().getReducedValueOrRaw());
1343 })
1344 .Case<arith::ConstantIndexOp>([&](auto indexConst) {
1345 return DynamicAPInt(indexConst.value());
1346 })
1347 .Case<arith::ConstantIntOp>([&](auto intConst) {
1348 auto valAttr = dyn_cast<IntegerAttr>(intConst.getValue());
1349 ensure(valAttr != nullptr, "arith::ConstantIntOp must have an IntegerAttr as its value");
1350 return integerAttrToDynamicAPInt(valAttr);
1351 })
1352 .Default([](auto *illegalOp) {
1353 std::string err;
1354 debug::Appender(err) << "unhandled getConst case: " << *illegalOp;
1355 llvm::report_fatal_error(Twine(err));
1356 return llvm::DynamicAPInt();
1357 });
1358 // clang-format on
1359 return fieldConst;
1360}
1361
1362ExpressionValue IntervalDataFlowAnalysis::performBinaryArithmetic(
1363 Operation *op, const LatticeValue &a, const LatticeValue &b
1364) {
1365 ensure(isArithmeticOp(op), "is not arithmetic op");
1366
1367 auto lhs = a.getScalarValue(), rhs = b.getScalarValue();
1368 ensure(lhs.getExpr(), "cannot perform arithmetic over null lhs smt expr");
1369 ensure(rhs.getExpr(), "cannot perform arithmetic over null rhs smt expr");
1370
1371 // clang-format off
1372 auto res = TypeSwitch<Operation *, ExpressionValue>(op)
1373 .Case<AddFeltOp>([&](auto) { return add(smtSolver, lhs, rhs); })
1374 .Case<SubFeltOp>([&](auto) { return sub(smtSolver, lhs, rhs); })
1375 .Case<MulFeltOp>([&](auto) { return mul(smtSolver, lhs, rhs); })
1376 .Case<DivFeltOp>([&](auto) {return div(smtSolver, op, lhs, rhs); })
1377 .Case<UnsignedIntDivFeltOp>([&](auto) {return uintDiv(smtSolver, op, lhs, rhs); })
1378 .Case<SignedIntDivFeltOp>([&](auto) {return sintDiv(smtSolver, op, lhs, rhs); })
1379 .Case<UnsignedModFeltOp>([&](auto) { return mod(smtSolver, lhs, rhs); })
1380 .Case<SignedModFeltOp>([&](auto) { return sintMod(smtSolver, lhs, rhs); })
1381 .Case<AndFeltOp>([&](auto) { return bitAnd(smtSolver, lhs, rhs); })
1382 .Case<OrFeltOp>([&](auto) { return bitOr(smtSolver, lhs, rhs); })
1383 .Case<XorFeltOp, arith::XOrIOp>([&](auto) { return bitXor(smtSolver, lhs, rhs); })
1384 .Case<ShlFeltOp>([&](auto) { return shiftLeft(smtSolver, lhs, rhs); })
1385 .Case<ShrFeltOp>([&](auto) { return shiftRight(smtSolver, lhs, rhs); })
1386 .Case<CmpOp>([&](auto cmpOp) { return cmp(smtSolver, cmpOp, lhs, rhs); })
1387 .Case<AndBoolOp>([&](auto) { return boolAnd(smtSolver, lhs, rhs); })
1388 .Case<OrBoolOp>([&](auto) { return boolOr(smtSolver, lhs, rhs); })
1389 .Case<XorBoolOp>([&](auto) { return boolXor(smtSolver, lhs, rhs); })
1390 .Default([&](auto *unsupported) {
1391 unsupported
1392 ->emitError(
1393 "unsupported binary arithmetic operation"
1394 )
1395 .report();
1396 return ExpressionValue();
1397 });
1398 // clang-format on
1399
1400 ensure(res.getExpr(), "arithmetic produced null smt expr");
1401 return res;
1402}
1403
1405IntervalDataFlowAnalysis::performUnaryArithmetic(Operation *op, const LatticeValue &a) {
1406 ensure(isArithmeticOp(op), "is not arithmetic op");
1407
1408 auto val = a.getScalarValue();
1409 ensure(val.getExpr(), "cannot perform arithmetic over null smt expr");
1410
1411 auto res = TypeSwitch<Operation *, ExpressionValue>(op)
1412 .Case<NegFeltOp>([&](auto) { return neg(smtSolver, val); })
1413 .Case<NotFeltOp>([&](auto) { return notOp(smtSolver, val); })
1414 .Case<NotBoolOp>([&](auto) { return boolNot(smtSolver, val); })
1415 // The inverse op is currently overapproximated
1416 .Case<InvFeltOp>([&](auto inv) {
1417 return fallbackUnaryOp(smtSolver, inv, val);
1418 }).Default([&](auto *unsupported) {
1419 unsupported
1420 ->emitWarning(
1421 "unsupported unary arithmetic operation, defaulting to over-approximated interval"
1422 )
1423 .report();
1424 return fallbackUnaryOp(smtSolver, unsupported, val);
1425 });
1426
1427 ensure(res.getExpr(), "arithmetic produced null smt expr");
1428 return res;
1429}
1430
1431void IntervalDataFlowAnalysis::applyInterval(Operation *valUser, Value val, Interval newInterval) {
1432 Lattice *valLattice = getLatticeElement(val);
1433 ExpressionValue oldLatticeVal = valLattice->getValue().getScalarValue();
1434 // Intersect with the current value to accumulate restrictions across constraints.
1435 Interval intersection = oldLatticeVal.getInterval().intersect(newInterval);
1436 ExpressionValue newLatticeVal = refineReducedInterval(oldLatticeVal, intersection);
1437 ChangeResult changed = valLattice->setValue(newLatticeVal);
1438
1439 if (auto blockArg = llvm::dyn_cast<BlockArgument>(val)) {
1440 auto fnOp = dyn_cast<FuncDefOp>(blockArg.getOwner()->getParentOp());
1441
1442 // Apply the interval from the constrain function inputs to the compute function inputs
1443 if (propagateInputConstraints && fnOp && fnOp.isStructConstrain() &&
1444 blockArg.getArgNumber() > 0 && !newInterval.isEntire()) {
1445 auto structOp = fnOp->getParentOfType<StructDefOp>();
1446 FuncDefOp computeFn = structOp.getComputeFuncOp();
1447 BlockArgument computeArg = computeFn.getArgument(blockArg.getArgNumber() - 1);
1448 Lattice *computeEntryLattice = getLatticeElement(computeArg);
1449
1450 SourceRef ref(computeArg);
1451 ExpressionValue newArgVal(
1452 getOrCreateSymbol(ref), newInterval,
1453 trackUnreducedIntervals ? std::optional<UnreducedInterval>(newInterval.firstUnreduced())
1454 : std::nullopt
1455 );
1456 propagateIfChanged(computeEntryLattice, computeEntryLattice->setValue(newArgVal));
1457 }
1458 }
1459
1460 // Now we descend into val's operands, if it has any.
1461 Operation *definingOp = val.getDefiningOp();
1462 if (!definingOp) {
1463 propagateIfChanged(valLattice, changed);
1464 return;
1465 }
1466
1467 const Field &f = field.get();
1468
1469 // This is a rules-based operation. If we have a rule for a given operation,
1470 // then we can make some kind of update, otherwise we leave the intervals
1471 // as is.
1472 // - First we'll define all the rules so the type switch can be less messy
1473
1474 // cmp.<pred> restricts each side of the comparison if the result is known.
1475 auto cmpCase = [&](CmpOp cmpOp) {
1476 // Cmp output range is [0, 1], so in order to do something, we must have newInterval
1477 // either "true" (1) or "false" (0).
1478 // -- In the case of a contradictory circuit, however, the cmp result is allowed
1479 // to be empty.
1480 ensure(
1481 newInterval.isBoolean() || newInterval.isEmpty(),
1482 "new interval for CmpOp is not boolean or empty"
1483 );
1484 if (!newInterval.isDegenerate()) {
1485 // The comparison result is unknown, so we can't update the operand ranges
1486 return;
1487 }
1488
1489 bool cmpTrue = newInterval.rhs() == f.one();
1490
1491 Value lhs = cmpOp.getLhs(), rhs = cmpOp.getRhs();
1492 auto lhsLat = getLatticeElement(lhs), rhsLat = getLatticeElement(rhs);
1493 ExpressionValue lhsExpr = lhsLat->getValue().getScalarValue(),
1494 rhsExpr = rhsLat->getValue().getScalarValue();
1495
1496 Interval newLhsInterval, newRhsInterval;
1497 const Interval &lhsInterval = lhsExpr.getInterval();
1498 const Interval &rhsInterval = rhsExpr.getInterval();
1499
1500 FeltCmpPredicate pred = cmpOp.getPredicate();
1501 // predicate cases
1502 auto eqCase = [&]() {
1503 return (pred == FeltCmpPredicate::EQ && cmpTrue) ||
1504 (pred == FeltCmpPredicate::NE && !cmpTrue);
1505 };
1506 auto neCase = [&]() {
1507 return (pred == FeltCmpPredicate::NE && cmpTrue) ||
1508 (pred == FeltCmpPredicate::EQ && !cmpTrue);
1509 };
1510 auto ltCase = [&]() {
1511 return (pred == FeltCmpPredicate::LT && cmpTrue) ||
1512 (pred == FeltCmpPredicate::GE && !cmpTrue);
1513 };
1514 auto leCase = [&]() {
1515 return (pred == FeltCmpPredicate::LE && cmpTrue) ||
1516 (pred == FeltCmpPredicate::GT && !cmpTrue);
1517 };
1518 auto gtCase = [&]() {
1519 return (pred == FeltCmpPredicate::GT && cmpTrue) ||
1520 (pred == FeltCmpPredicate::LE && !cmpTrue);
1521 };
1522 auto geCase = [&]() {
1523 return (pred == FeltCmpPredicate::GE && cmpTrue) ||
1524 (pred == FeltCmpPredicate::LT && !cmpTrue);
1525 };
1526
1527 // new intervals based on case
1528 if (eqCase()) {
1529 newLhsInterval = newRhsInterval = lhsInterval.intersect(rhsInterval);
1530 } else if (neCase()) {
1531 if (lhsInterval.isDegenerate() && rhsInterval.isDegenerate() && lhsInterval == rhsInterval) {
1532 // In this case, we know lhs and rhs cannot satisfy this assertion, so they have
1533 // an empty value range.
1534 newLhsInterval = newRhsInterval = Interval::Empty(f);
1535 } else if (lhsInterval.isDegenerate()) {
1536 // rhs must not overlap with lhs
1537 newLhsInterval = lhsInterval;
1538 newRhsInterval = rhsInterval.difference(lhsInterval);
1539 } else if (rhsInterval.isDegenerate()) {
1540 // lhs must not overlap with rhs
1541 newLhsInterval = lhsInterval.difference(rhsInterval);
1542 newRhsInterval = rhsInterval;
1543 } else {
1544 // Leave unchanged
1545 newLhsInterval = lhsInterval;
1546 newRhsInterval = rhsInterval;
1547 }
1548 } else if (ltCase()) {
1549 newLhsInterval = lhsInterval.toUnreduced().computeLTPart(rhsInterval.toUnreduced()).reduce(f);
1550 newRhsInterval = rhsInterval.toUnreduced().computeGEPart(lhsInterval.toUnreduced()).reduce(f);
1551 } else if (leCase()) {
1552 newLhsInterval = lhsInterval.toUnreduced().computeLEPart(rhsInterval.toUnreduced()).reduce(f);
1553 newRhsInterval = rhsInterval.toUnreduced().computeGTPart(lhsInterval.toUnreduced()).reduce(f);
1554 } else if (gtCase()) {
1555 newLhsInterval = lhsInterval.toUnreduced().computeGTPart(rhsInterval.toUnreduced()).reduce(f);
1556 newRhsInterval = rhsInterval.toUnreduced().computeLEPart(lhsInterval.toUnreduced()).reduce(f);
1557 } else if (geCase()) {
1558 newLhsInterval = lhsInterval.toUnreduced().computeGEPart(rhsInterval.toUnreduced()).reduce(f);
1559 newRhsInterval = rhsInterval.toUnreduced().computeLTPart(lhsInterval.toUnreduced()).reduce(f);
1560 } else {
1561 cmpOp->emitWarning("unhandled cmp predicate").report();
1562 return;
1563 }
1564
1565 // Now we recurse to each operand
1566 applyInterval(cmpOp, lhs, newLhsInterval);
1567 applyInterval(cmpOp, rhs, newRhsInterval);
1568 };
1569
1570 // Multiplication cases:
1571 // - If the result of a multiplication is non-zero, then both operands must be
1572 // non-zero.
1573 // - If one operand is a constant, we can propagate the new interval when multiplied
1574 // by the multiplicative inverse of the constant.
1575 auto mulCase = [&](MulFeltOp mulOp) {
1576 // We check for the constant case first.
1577 auto constCase = [&](FeltConstantOp constOperand, Value multiplicand) {
1578 DynamicAPInt constVal = getConst(constOperand);
1579 if (constVal == 0) {
1580 // There's no inverse for zero, so we do nothing.
1581 return;
1582 }
1583 Interval updatedInterval = newInterval * Interval::Degenerate(f, f.inv(constVal));
1584 applyInterval(mulOp, multiplicand, updatedInterval);
1585 };
1586
1587 Value lhs = mulOp.getLhs(), rhs = mulOp.getRhs();
1588
1589 auto lhsConstOp = dyn_cast_if_present<FeltConstantOp>(lhs.getDefiningOp());
1590 auto rhsConstOp = dyn_cast_if_present<FeltConstantOp>(rhs.getDefiningOp());
1591 // If both are consts, we don't need to do anything
1592 if (lhsConstOp && rhsConstOp) {
1593 return;
1594 } else if (lhsConstOp) {
1595 constCase(lhsConstOp, rhs);
1596 return;
1597 } else if (rhsConstOp) {
1598 constCase(rhsConstOp, lhs);
1599 return;
1600 }
1601
1602 // Otherwise, try to propagate non-zero information.
1603 auto zeroInt = Interval::Degenerate(f, f.zero());
1604 if (newInterval.intersect(zeroInt).isNotEmpty()) {
1605 // The multiplication may be zero, so we can't reduce the operands to be non-zero
1606 return;
1607 }
1608
1609 auto lhsLat = getLatticeElement(lhs), rhsLat = getLatticeElement(rhs);
1610 ExpressionValue lhsExpr = lhsLat->getValue().getScalarValue(),
1611 rhsExpr = rhsLat->getValue().getScalarValue();
1612 Interval newLhsInterval = lhsExpr.getInterval().difference(zeroInt);
1613 Interval newRhsInterval = rhsExpr.getInterval().difference(zeroInt);
1614 applyInterval(mulOp, lhs, newLhsInterval);
1615 applyInterval(mulOp, rhs, newRhsInterval);
1616 };
1617
1618 auto addCase = [&](AddFeltOp addOp) {
1619 Value lhs = addOp.getLhs(), rhs = addOp.getRhs();
1620 Lattice *lhsLat = getLatticeElement(lhs), *rhsLat = getLatticeElement(rhs);
1621 ExpressionValue lhsVal = lhsLat->getValue().getScalarValue();
1622 ExpressionValue rhsVal = rhsLat->getValue().getScalarValue();
1623
1624 const Interval &currLhsInt = lhsVal.getInterval(), &currRhsInt = rhsVal.getInterval();
1625
1626 Interval derivedLhsInt = newInterval - currRhsInt;
1627 Interval derivedRhsInt = newInterval - currLhsInt;
1628
1629 Interval finalLhsInt = currLhsInt.intersect(derivedLhsInt);
1630 Interval finalRhsInt = currRhsInt.intersect(derivedRhsInt);
1631
1632 applyInterval(addOp, lhs, finalLhsInt);
1633 applyInterval(addOp, rhs, finalRhsInt);
1634 };
1635
1636 auto subCase = [&](SubFeltOp subOp) {
1637 Value lhs = subOp.getLhs(), rhs = subOp.getRhs();
1638 Lattice *lhsLat = getLatticeElement(lhs), *rhsLat = getLatticeElement(rhs);
1639 ExpressionValue lhsVal = lhsLat->getValue().getScalarValue();
1640 ExpressionValue rhsVal = rhsLat->getValue().getScalarValue();
1641
1642 const Interval &currLhsInt = lhsVal.getInterval(), &currRhsInt = rhsVal.getInterval();
1643
1644 Interval derivedLhsInt = newInterval + currRhsInt;
1645 Interval derivedRhsInt = currLhsInt - newInterval;
1646
1647 Interval finalLhsInt = currLhsInt.intersect(derivedLhsInt);
1648 Interval finalRhsInt = currRhsInt.intersect(derivedRhsInt);
1649
1650 applyInterval(subOp, lhs, finalLhsInt);
1651 applyInterval(subOp, rhs, finalRhsInt);
1652 };
1653
1654 auto selectCase = [&](arith::SelectOp selectOp) {
1655 Value cond = selectOp.getCondition();
1656 Value trueVal = selectOp.getTrueValue();
1657 Value falseVal = selectOp.getFalseValue();
1658
1659 ExpressionValue condExpr = getLatticeElement(cond)->getValue().getScalarValue();
1660 ExpressionValue trueExpr = getLatticeElement(trueVal)->getValue().getScalarValue();
1661 ExpressionValue falseExpr = getLatticeElement(falseVal)->getValue().getScalarValue();
1662
1663 const Interval &condInterval = condExpr.getInterval();
1664 if (condInterval.isDegenerate() && condInterval.rhs() == f.one()) {
1665 applyInterval(selectOp, trueVal, newInterval);
1666 return;
1667 }
1668 if (condInterval.isDegenerate() && condInterval.rhs() == f.zero()) {
1669 applyInterval(selectOp, falseVal, newInterval);
1670 return;
1671 }
1672
1673 Interval trueOverlap = trueExpr.getInterval().intersect(newInterval);
1674 Interval falseOverlap = falseExpr.getInterval().intersect(newInterval);
1675 bool truePossible = trueOverlap.isNotEmpty();
1676 bool falsePossible = falseOverlap.isNotEmpty();
1677
1678 if (truePossible && !falsePossible) {
1679 applyInterval(selectOp, cond, Interval::True(f));
1680 applyInterval(selectOp, trueVal, newInterval);
1681 return;
1682 }
1683 if (!truePossible && falsePossible) {
1684 applyInterval(selectOp, cond, Interval::False(f));
1685 applyInterval(selectOp, falseVal, newInterval);
1686 return;
1687 }
1688 if (!truePossible && !falsePossible) {
1689 applyInterval(selectOp, cond, Interval::Empty(f));
1690 }
1691 };
1692
1693 auto readmCase = [&](MemberReadOp) {
1694 SourceRefLatticeValue sourceRefVal = getSourceRefState(val);
1695
1696 if (sourceRefVal.isSingleValue()) {
1697 const SourceRef &ref = sourceRefVal.getSingleValue();
1698 readResults[ref].insert(valLattice);
1699
1700 // Also propagate to all other member read results for this member
1701 for (Lattice *l : readResults[ref]) {
1702 if (l != valLattice) {
1703 propagateIfChanged(l, l->setValue(newLatticeVal));
1704 }
1705 }
1706 }
1707 };
1708
1709 auto readArrCase = [&](ReadArrayOp) {
1710 auto arrayRef = getArrayAccessRef(valUser, llvm::cast<ReadArrayOp>(definingOp));
1711 if (succeeded(arrayRef)) {
1712 readResults[*arrayRef].insert(valLattice);
1713
1714 for (Lattice *l : readResults[*arrayRef]) {
1715 if (l != valLattice) {
1716 propagateIfChanged(l, l->setValue(newLatticeVal));
1717 }
1718 }
1719 }
1720
1721 SourceRefLatticeValue sourceRefVal = getSourceRefState(val);
1722
1723 if (sourceRefVal.isSingleValue()) {
1724 const SourceRef &ref = sourceRefVal.getSingleValue();
1725 readResults[ref].insert(valLattice);
1726
1727 // Also propagate to all other member read results for this member
1728 for (Lattice *l : readResults[ref]) {
1729 if (l != valLattice) {
1730 propagateIfChanged(l, l->setValue(newLatticeVal));
1731 }
1732 }
1733 }
1734 };
1735
1736 // Pass the interval to the operand when the cast preserves its value.
1737 // Boolean inputs cannot overflow in any IntToFeltOp mode.
1738 auto castCase = [&](Operation *op) {
1739 if (auto intToFelt = dyn_cast<IntToFeltOp>(op);
1740 intToFelt && intToFelt.getOverflow() != cast::OverflowSemantics::ASSERT &&
1741 !intToFelt.getValue().getType().isInteger(1)) {
1742 return;
1743 }
1744 if (auto feltToIndex = dyn_cast<FeltToIndexOp>(op);
1745 feltToIndex && feltToIndex.getOverflow() != cast::OverflowSemantics::ASSERT) {
1746 return;
1747 }
1748 applyInterval(op, op->getOperand(0), newInterval);
1749 };
1750
1751 // - Apply the rules given the op.
1752 // NOTE: disabling clang-format for this because it makes the last case statement
1753 // look ugly.
1754 // clang-format off
1755 TypeSwitch<Operation *>(definingOp)
1756 .Case<CmpOp>([&](auto op) { cmpCase(op); })
1757 .Case<AddFeltOp>([&](auto op) { return addCase(op); })
1758 .Case<SubFeltOp>([&](auto op) { return subCase(op); })
1759 .Case<MulFeltOp>([&](auto op) { mulCase(op); })
1760 .Case<arith::SelectOp>([&](auto op) { selectCase(op); })
1761 .Case<MemberReadOp>([&](auto op){ readmCase(op); })
1762 .Case<ReadArrayOp>([&](auto op){ readArrCase(op); })
1763 .Case<IntToFeltOp, FeltToIndexOp>([&](auto op) { castCase(op); })
1764 .Default([&](Operation *) { });
1765 // clang-format on
1766
1767 // Propagate after recursion to avoid having recursive calls unset the value.
1768 propagateIfChanged(valLattice, changed);
1769}
1770
1771FailureOr<std::pair<DenseSet<Value>, Interval>>
1772IntervalDataFlowAnalysis::getGeneralizedDecompInterval(
1773 Operation * /*baseOp*/, Value lhs, Value rhs
1774) {
1775 auto isZeroConst = [this](Value v) {
1776 Operation *op = v.getDefiningOp();
1777 if (!op) {
1778 return false;
1779 }
1780 if (!isConstOp(op)) {
1781 return false;
1782 }
1783 return getConst(op) == field.get().zero();
1784 };
1785 bool lhsIsZero = isZeroConst(lhs), rhsIsZero = isZeroConst(rhs);
1786 Value exprTree = nullptr;
1787 if (lhsIsZero && !rhsIsZero) {
1788 exprTree = rhs;
1789 } else if (!lhsIsZero && rhsIsZero) {
1790 exprTree = lhs;
1791 } else {
1792 return failure();
1793 }
1794
1795 // We now explore the expression tree for multiplications of subtractions/signal values.
1796 std::optional<SourceRef> signalRef = std::nullopt;
1797 DenseSet<Value> signalVals;
1798 SmallVector<DynamicAPInt> consts;
1799 SmallVector<Value> frontier {exprTree};
1800 while (!frontier.empty()) {
1801 Value v = frontier.back();
1802 frontier.pop_back();
1803 Operation *op = v.getDefiningOp();
1804
1805 FeltConstantOp c;
1806 Value signalVal;
1807 auto handleRefValue = [this, &signalRef, &signalVal, &signalVals]() {
1808 SourceRefLatticeValue refSet = getSourceRefState(signalVal);
1809 if (!refSet.isScalar() || !refSet.isSingleValue()) {
1810 return failure();
1811 }
1812 SourceRef r = refSet.getSingleValue();
1813 if (signalRef.has_value() && signalRef.value() != r) {
1814 return failure();
1815 } else if (!signalRef.has_value()) {
1816 signalRef = r;
1817 }
1818 signalVals.insert(signalVal);
1819 return success();
1820 };
1821
1822 auto subPattern = m_CommutativeOp<SubFeltOp>(m_RefValue(&signalVal), m_Constant(&c));
1823 if (op && matchPattern(op, subPattern)) {
1824 if (failed(handleRefValue())) {
1825 return failure();
1826 }
1827 consts.push_back(getConst(c));
1828 continue;
1829 } else if (m_RefValue(&signalVal).match(v)) {
1830 if (failed(handleRefValue())) {
1831 return failure();
1832 }
1833 consts.push_back(field.get().zero());
1834 continue;
1835 }
1836
1837 Value a, b;
1838 auto mulPattern = m_CommutativeOp<MulFeltOp>(matchers::m_Any(&a), matchers::m_Any(&b));
1839 if (op && matchPattern(op, mulPattern)) {
1840 frontier.push_back(a);
1841 frontier.push_back(b);
1842 continue;
1843 }
1844
1845 return failure();
1846 }
1847
1848 // Now, we aggregate the Interval. If we have sparse values (e.g., 0, 2, 4),
1849 // we will create a larger range of [0, 4], since we don't support multiple intervals.
1850 std::sort(consts.begin(), consts.end());
1851 Interval iv = UnreducedInterval(consts.front(), consts.back()).reduce(field.get());
1852 return std::make_pair(std::move(signalVals), iv);
1853}
1854
1855/* StructIntervals */
1856
1858 mlir::DataFlowSolver &solver, mlir::AnalysisManager &am, const IntervalAnalysisContext &ctx
1859) {
1860 SymbolTableCollection tables;
1861
1862 auto computeIntervalsImpl =
1863 [&solver, &am, &ctx, &tables, this](
1864 FuncDefOp fn, llvm::MapVector<SourceRef, Interval> &memberRanges,
1865 llvm::MapVector<SourceRef, UnreducedInterval> &memberUnreducedRanges,
1866 llvm::SetVector<ExpressionValue> & /*solverConstraints*/
1867 ) {
1868 auto setUnreducedRange =
1869 [&memberUnreducedRanges](const SourceRef &ref, const UnreducedInterval &interval) {
1870 memberUnreducedRanges.erase(ref);
1871 memberUnreducedRanges.insert({ref, interval});
1872 };
1873 // Since every lattice value does not contain every value, we will traverse
1874 // the function backwards (from most up-to-date to least-up-to-date lattices)
1875 // searching for the source refs. Once a source ref is found, we remove it
1876 // from the search set.
1877
1878 SourceRefSet searchSet;
1879 for (const auto &ref : SourceRef::getAllSourceRefs(structDef, fn)) {
1880 // We only want to compute intervals for field elements and not composite types.
1881 if (!ref.isScalar()) {
1882 continue;
1883 }
1884 searchSet.insert(ref);
1885 }
1886 SourceRefSet functionRefs = searchSet;
1887
1888 auto mergeInterval = [&memberRanges, &memberUnreducedRanges](
1889 const SourceRef &ref, const Interval &interval,
1890 std::optional<UnreducedInterval> unreducedInterval = std::nullopt
1891 ) {
1892 auto *existing = memberRanges.find(ref);
1893 if (existing != memberRanges.end()) {
1894 Interval mergedInterval = existing->second.intersect(interval);
1895 bool intervalChanged = mergedInterval != existing->second;
1896 existing->second = std::move(mergedInterval);
1897
1898 if (unreducedInterval.has_value()) {
1899 auto *existingUnreduced = memberUnreducedRanges.find(ref);
1900 if (existingUnreduced != memberUnreducedRanges.end()) {
1901 existingUnreduced->second = existingUnreduced->second.intersect(*unreducedInterval);
1902 } else {
1903 memberUnreducedRanges.insert({ref, *unreducedInterval});
1904 }
1905 } else if (intervalChanged) {
1906 memberUnreducedRanges.erase(ref);
1907 }
1908 return;
1909 }
1910
1911 memberRanges[ref] = interval;
1912 if (unreducedInterval.has_value()) {
1913 memberUnreducedRanges.insert({ref, *unreducedInterval});
1914 }
1915 };
1916
1917 // Iterate over arguments
1918 for (BlockArgument arg : fn.getArguments()) {
1919 SourceRef ref {arg};
1920 if (searchSet.erase(ref)) {
1921 const IntervalAnalysisLattice *lattice = solver.lookupState<IntervalAnalysisLattice>(arg);
1922 // If we never referenced this argument, use a default value
1923 ExpressionValue expr = lattice->getValue().getScalarValue();
1924 if (!expr.getExpr()) {
1925 expr = expr.withInterval(Interval::Entire(ctx.getField()));
1926 if (ctx.doTrackUnreducedIntervals()) {
1927 expr = expr.withUnreducedInterval(expr.getInterval().firstUnreduced());
1928 }
1929 }
1930 memberRanges[ref] = expr.getInterval();
1931 if (expr.hasUnreducedInterval()) {
1932 setUnreducedRange(ref, expr.getUnreducedInterval());
1933 }
1934 assert(memberRanges[ref].getField() == ctx.getField() && "bad interval defaults");
1935 }
1936 }
1937
1938 // Aggregate all read intervals for a ref. A single ref may be read at multiple program
1939 // points with different precision, so picking an arbitrary lattice from the DenseSet is
1940 // nondeterministic. Joining preserves the overapproximation regardless of iteration order.
1941 for (const auto &[ref, lattices] : ctx.intervalDFA->getReadResults()) {
1942 if (!lattices.empty() && searchSet.erase(ref)) {
1943 Interval joinedInterval = Interval::Empty(ctx.getField());
1944 std::optional<UnreducedInterval> joinedUnreduced = std::nullopt;
1945 bool sawFirst = false;
1946 for (const IntervalAnalysisLattice *lattice : lattices) {
1947 const ExpressionValue &expr = lattice->getValue().getScalarValue();
1948 joinedInterval = joinedInterval.join(expr.getInterval());
1949 if (!sawFirst) {
1950 joinedUnreduced = expr.getOptionalUnreducedInterval();
1951 sawFirst = true;
1952 } else {
1953 joinedUnreduced =
1954 mergeUnreducedIntervals(joinedUnreduced, expr.getOptionalUnreducedInterval());
1955 }
1956 }
1957 memberRanges[ref] = joinedInterval;
1958 if (joinedUnreduced.has_value()) {
1959 setUnreducedRange(ref, *joinedUnreduced);
1960 }
1961 assert(memberRanges[ref].getField() == ctx.getField() && "bad interval defaults");
1962 }
1963 }
1964
1965 for (const auto &[ref, val] : ctx.intervalDFA->getWriteResults()) {
1966 if (searchSet.erase(ref)) {
1967 memberRanges[ref] = val.getInterval();
1968 if (val.hasUnreducedInterval()) {
1969 setUnreducedRange(ref, val.getUnreducedInterval());
1970 }
1971 assert(memberRanges[ref].getField() == ctx.getField() && "bad interval defaults");
1972 }
1973 }
1974
1975 // Child constrain calls refine parent-visible storage, but only after the callee
1976 // summary is translated through the call operands. If translation cannot prove
1977 // which parent ref owns a child interval, the local overapproximation remains.
1978 if (fn.isStructConstrain()) {
1979 auto mergeChildConstrainIntervals = [&](CallOp fnCall) {
1980 if (!dataflow::isOperationLive(solver, fnCall.getOperation())) {
1981 return;
1982 }
1983
1984 auto res = resolveCallableSilently<FuncDefOp>(tables, fnCall);
1985 if (failed(res)) {
1986 return;
1987 }
1988
1989 FuncDefOp calledFn = res->get();
1990 if (!calledFn.isStructConstrain()) {
1991 return;
1992 }
1993
1994 auto calledStruct = calledFn->getParentOfType<StructDefOp>();
1995 if (calledStruct == structDef) {
1996 return;
1997 }
1998
1999 auto &childAnalysis = am.getChildAnalysis<StructIntervalAnalysis>(calledStruct);
2000 if (childAnalysis.inProgress(ctx)) {
2001 return;
2002 }
2003 if (!childAnalysis.constructed(ctx)) {
2004 ensure(
2005 succeeded(childAnalysis.runAnalysis(solver, am, ctx)),
2006 "could not construct interval analysis for child struct"
2007 );
2008 }
2009
2010 // Translate callee argument refs into parent refs and capture scalar call-site intervals
2011 // that can refine direct equality groups inside the child constrain function.
2012 SourceRefRemappings identityTranslations;
2013 llvm::MapVector<SourceRef, Interval> callOperandIntervals;
2014 for (unsigned i = 0; i < calledFn.getNumArguments(); i++) {
2015 SourceRef prefix(calledFn.getArgument(i));
2016 Value operand = fnCall.getOperand(i);
2017 std::optional<SourceRefLatticeValue> identityVal =
2018 getIdentitySourceRefState(solver, operand);
2019 if (identityVal.has_value()) {
2020 identityTranslations.push_back({prefix, *identityVal});
2021 }
2022
2023 if (!llvm::isa<ArrayType, StructType, pod::PodType>(operand.getType())) {
2024 const IntervalAnalysisLattice *lattice =
2025 solver.lookupState<IntervalAnalysisLattice>(operand);
2026 if (lattice != nullptr) {
2027 const ExpressionValue &expr = lattice->getValue().getScalarValue();
2028 callOperandIntervals[prefix] = expr.getInterval();
2029 }
2030 }
2031 }
2032
2033 const StructIntervals &childIntervals = childAnalysis.getResult(ctx);
2034 const auto &constrainIntervals = childIntervals.getConstrainIntervals();
2035 const auto &constrainUnreducedIntervals = childIntervals.getConstrainUnreducedIntervals();
2036 for (const auto &[childRef, childInterval] : constrainIntervals) {
2037 auto translatedRefs = translateRef(childRef, identityTranslations);
2038 if (failed(translatedRefs)) {
2039 continue;
2040 }
2041
2042 std::optional<UnreducedInterval> childUnreduced = std::nullopt;
2043 if (const auto *childUnreducedIt = constrainUnreducedIntervals.find(childRef);
2044 childUnreducedIt != constrainUnreducedIntervals.end()) {
2045 childUnreduced = childUnreducedIt->second;
2046 }
2047
2048 SourceRefSet uniqueTranslatedRefs;
2049 for (const SourceRef &translatedRef : *translatedRefs) {
2050 uniqueTranslatedRefs.insert(translatedRef);
2051 }
2052 if (uniqueTranslatedRefs.size() != 1) {
2053 continue;
2054 }
2055
2056 const SourceRef &translatedRef = *uniqueTranslatedRefs.begin();
2057 if (functionRefs.contains(translatedRef)) {
2058 mergeInterval(translatedRef, childInterval, childUnreduced);
2059 searchSet.erase(translatedRef);
2060 }
2061 }
2062
2063 // Direct equalities in the child can combine child summaries, call operand intervals, and
2064 // existing parent intervals before merging back into each translated parent ref.
2065 llvm::EquivalenceClasses<SourceRef> directEqRefs =
2066 collectDirectEqualityRefs(solver, calledFn);
2067 for (const auto *leader : directEqRefs) {
2068 if (!leader->isLeader()) {
2069 continue;
2070 }
2071
2072 llvm::MapVector<SourceRef, Interval> translatedEqRefs;
2073 Interval contextualInterval = Interval::Entire(ctx.getField());
2074 bool hasInterval = false;
2075 bool ambiguousTranslation = false;
2076
2077 for (const SourceRef &member : directEqRefs.members(*leader)) {
2078 Interval memberInterval = Interval::Entire(ctx.getField());
2079 if (const auto *childIntervalIt = constrainIntervals.find(member);
2080 childIntervalIt != constrainIntervals.end()) {
2081 memberInterval = memberInterval.intersect(childIntervalIt->second);
2082 }
2083 if (auto *callOperandIt = callOperandIntervals.find(member);
2084 callOperandIt != callOperandIntervals.end()) {
2085 memberInterval = memberInterval.intersect(callOperandIt->second);
2086 contextualInterval = contextualInterval.intersect(memberInterval);
2087 hasInterval = true;
2088 }
2089
2090 auto translatedRefs = translateRef(member, identityTranslations);
2091 if (failed(translatedRefs)) {
2092 continue;
2093 }
2094
2095 SourceRefSet uniqueTranslatedRefs;
2096 for (const SourceRef &translatedRef : *translatedRefs) {
2097 uniqueTranslatedRefs.insert(translatedRef);
2098 }
2099 if (uniqueTranslatedRefs.size() != 1) {
2100 ambiguousTranslation = true;
2101 break;
2102 }
2103
2104 const SourceRef &translatedRef = *uniqueTranslatedRefs.begin();
2105 if (!functionRefs.contains(translatedRef)) {
2106 continue;
2107 }
2108
2109 if (auto *parentIntervalIt = memberRanges.find(translatedRef);
2110 parentIntervalIt != memberRanges.end()) {
2111 memberInterval = memberInterval.intersect(parentIntervalIt->second);
2112 }
2113
2114 translatedEqRefs[translatedRef] = memberInterval;
2115 contextualInterval = contextualInterval.intersect(memberInterval);
2116 hasInterval = true;
2117 }
2118
2119 if (ambiguousTranslation || !hasInterval || translatedEqRefs.empty()) {
2120 continue;
2121 }
2122
2123 for (const auto &[translatedRef, _] : translatedEqRefs) {
2124 mergeInterval(translatedRef, contextualInterval);
2125 searchSet.erase(translatedRef);
2126 }
2127 }
2128 };
2129
2130 fn.walk(mergeChildConstrainIntervals);
2131 }
2132
2133 // For all unfound refs, default to the entire range.
2134 for (const auto &ref : searchSet) {
2135 memberRanges[ref] = Interval::Entire(ctx.getField());
2136 if (ctx.doTrackUnreducedIntervals()) {
2137 setUnreducedRange(ref, memberRanges[ref].firstUnreduced());
2138 }
2139 }
2140
2141 // Sort the outputs since we assembled things out of order.
2142 //
2143 // `llvm::MapVector` maintains an internal key -> index map. Sorting it in
2144 // place corrupts lookup semantics because the backing vector is reordered
2145 // without rebuilding that map. Reinsert into a fresh MapVector instead.
2146 llvm::SmallVector<std::pair<SourceRef, Interval>> sortedRanges;
2147 sortedRanges.reserve(memberRanges.size());
2148 for (const auto &[ref, interval] : memberRanges) {
2149 sortedRanges.emplace_back(ref, interval);
2150 }
2151 llvm::sort(sortedRanges, [](const auto &a, const auto &b) { return a.first < b.first; });
2152 llvm::SmallVector<std::pair<SourceRef, UnreducedInterval>> sortedUnreducedRanges;
2153 sortedUnreducedRanges.reserve(memberUnreducedRanges.size());
2154 for (const auto &[ref, interval] : memberUnreducedRanges) {
2155 sortedUnreducedRanges.emplace_back(ref, interval);
2156 }
2157 llvm::sort(sortedUnreducedRanges, [](const auto &a, const auto &b) {
2158 return a.first < b.first;
2159 });
2160 memberRanges.clear();
2161 memberUnreducedRanges.clear();
2162 for (auto &[ref, interval] : sortedRanges) {
2163 memberRanges[ref] = interval;
2164 }
2165 for (auto &[ref, interval] : sortedUnreducedRanges) {
2166 memberUnreducedRanges.insert({ref, interval});
2167 }
2168 };
2169
2170 if (auto computeFn = structDef.getComputeFuncOp()) {
2171 computeIntervalsImpl(
2172 computeFn, computeMemberRanges, computeMemberUnreducedRanges, computeSolverConstraints
2173 );
2174 }
2175 if (auto constrainFn = structDef.getConstrainFuncOp()) {
2176 computeIntervalsImpl(
2177 constrainFn, constrainMemberRanges, constrainMemberUnreducedRanges,
2178 constrainSolverConstraints
2179 );
2180 }
2181
2182 return success();
2183}
2184
2186 mlir::raw_ostream &os, bool withConstraints, bool printCompute, bool printUnreduced
2187) const {
2188 auto writeIntervals =
2189 [&os, &withConstraints, &printUnreduced](
2190 const char *fnName, const llvm::MapVector<SourceRef, Interval> &memberRanges,
2191 const llvm::MapVector<SourceRef, UnreducedInterval> &memberUnreducedRanges,
2192 const llvm::SetVector<ExpressionValue> &solverConstraints, bool printName
2193 ) {
2194 int indent = 4;
2195 if (printName) {
2196 os << '\n';
2197 os.indent(indent) << fnName << " {";
2198 indent += 4;
2199 }
2200
2201 if (memberRanges.empty()) {
2202 os << "}\n";
2203 return;
2204 }
2205
2206 for (const auto &[ref, interval] : memberRanges) {
2207 os << '\n';
2208 os.indent(indent) << ref << " in " << interval;
2209 if (printUnreduced) {
2210 const auto *unreducedIt = memberUnreducedRanges.find(ref);
2211 if (unreducedIt != memberUnreducedRanges.end()) {
2212 os << " ( " << unreducedIt->second << " )";
2213 }
2214 }
2215 }
2216
2217 if (withConstraints) {
2218 os << "\n\n";
2219 os.indent(indent) << "Solver Constraints { ";
2220 if (solverConstraints.empty()) {
2221 os << "}\n";
2222 } else {
2223 for (const auto &e : solverConstraints) {
2224 os << '\n';
2225 os.indent(indent + 4);
2226 e.getExpr()->print(os);
2227 }
2228 os << '\n';
2229 os.indent(indent) << '}';
2230 }
2231 }
2232
2233 if (printName) {
2234 os << '\n';
2235 os.indent(indent - 4) << '}';
2236 }
2237 };
2238
2239 os << "StructIntervals { ";
2240 if (constrainMemberRanges.empty() && (!printCompute || computeMemberRanges.empty())) {
2241 os << "}\n";
2242 return;
2243 }
2244
2245 if (printCompute) {
2246 writeIntervals(
2247 FUNC_NAME_COMPUTE, computeMemberRanges, computeMemberUnreducedRanges,
2248 computeSolverConstraints, printCompute
2249 );
2250 }
2251 writeIntervals(
2252 FUNC_NAME_CONSTRAIN, constrainMemberRanges, constrainMemberUnreducedRanges,
2253 constrainSolverConstraints, printCompute
2254 );
2255
2256 os << "\n}\n";
2257}
2258
2259} // 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 the zero field element.
Definition Field.h:79
llvm::DynamicAPInt prime() const
For the prime field p, returns p.
Definition Field.h:70
llvm::DynamicAPInt one() const
Returns the unit field element.
Definition Field.h:82
unsigned bitWidth() const
Width sufficient for the modulus and its representatives, independent of literal storage.
Definition Field.h:103
llvm::DynamicAPInt inv(const llvm::DynamicAPInt &i) const
Returns the multiplicative inverse of i in prime field p.
Definition Field.cpp:161
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:108
llvm::DynamicAPInt maxVal() const
Returns p - 1, which is the max value possible in a prime field described by p.
Definition Field.h:85
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:207
bool isEmpty() const
Definition Intervals.h:315
static Interval True(const Field &f)
Definition Intervals.h:226
llvm::DynamicAPInt rhs() const
Definition Intervals.h:343
Interval intersect(const Interval &rhs) const
Intersect.
UnreducedInterval toUnreduced() const
Convert to an UnreducedInterval.
static Interval Boolean(const Field &f)
Definition Intervals.h:228
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:230
bool isDegenerate() const
Definition Intervals.h:317
bool isNotEmpty() const
Definition Intervals.h:316
static Interval False(const Field &f)
Definition Intervals.h:224
llvm::DynamicAPInt lhs() const
Definition Intervals.h:342
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
static SourceRefIndex forArrayDimension(int64_t size)
Select every nonnegative index in a static or dynamic array dimension.
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:152
mlir::FailureOr< SourceRef > createChild(const SourceRefIndex &r) const
Definition SourceRef.h:384
bool isScalar() const
Definition SourceRef.h:263
std::vector< SourceRefIndex > Path
Definition SourceRef.h:154
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:1444
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:1099
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)
llvm::SMTExprRef createFieldInverseExpr(const llvm::SMTSolverRef &solver, Operation *op, const ExpressionValue &val, StringRef suffix="")
constexpr T checkedCast(U u) noexcept
Definition Compare.h:94
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)
llvm::DynamicAPInt integerAttrToDynamicAPInt(mlir::IntegerAttr attr)
Decode an MLIR integer using its signedness; signless i1 denotes a boolean.
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