Skip to content

Commit 6bfc273

Browse files
authored
Warn when quantifier range exceeds SAT solver threshold (#4579)
Emit a compile-time warning when an unbounded `forall!` or `exists!` quantifier is detected. SAT solvers (the default backend) expand quantifiers into one conjunction/disjunction per value in the range, so unbounded ranges (`forall!(|i| ...)` which expands over all 2^64 `usize` values) cause silent memory exhaustion or divergence with no diagnostic. Contributes to #3273. ## Problem Writing `forall!(|i| ...)` (unbounded, ranging over all `usize` values) with the default SAT backend silently hangs or OOMs because CBMC expands the quantifier into 2^64 terms. Users get no feedback about why verification doesn't terminate. ## Solution - **`warn_large_quantifier_range()`** in `hooks.rs`: At codegen time, checks if both quantifier bounds are compile-time integer constants. If the range exceeds `QUANTIFIER_RANGE_WARN_THRESHOLD` (1000), emits a `span_warn` suggesting tighter bounds or an SMT solver. For unbounded ranges (~2^64), uses a human-friendly message instead of the raw integer. - **`unwrap_to_constant()`**: Helper that unwraps typecasts and resolves symbol references via the symbol table to extract integer constants from bound expressions. Includes a depth limit (5) to guard against cycles. ### Known limitation Bounded quantifiers (`forall!(|i in (0, 2000)| ...)`) use `let` bindings in the macro expansion for type coercion, which hides the constant values from codegen. The warning currently only fires for **unbounded** quantifiers, which is the most dangerous case. Detecting large bounded ranges would require constant propagation through local variables, a potential follow-up improvement. ## Example warning ``` warning: `kani::forall` has an unbounded (~2^64) range; SAT solvers (the default backend) expand quantifiers over every value and may exhaust memory or diverge. Consider adding tighter bounds or using an SMT solver (`#[kani::solver(z3)]` or `--solver z3`). --> tests/expected/quantifiers/large_range_warning.rs:15:13 ``` ## Testing - `large_range_warning.rs` — expected test with 3 harnesses: - `check_unbounded_forall_warns`: unbounded `forall!(|i| ...)`, verifies warning is emitted - `check_unbounded_exists_warns`: unbounded `exists!(|i| ...)`, verifies warning is emitted - `check_small_forall_no_warn`: range of 10, verifies no warning (absence of output is intentional) --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.
1 parent d2c6dca commit 6bfc273

3 files changed

Lines changed: 119 additions & 0 deletions

File tree

kani-compiler/src/codegen_cprover_gotoc/overrides/hooks.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,12 @@ fn handle_quantifier(
957957
let lower_bound = &fargs[0];
958958
let upper_bound = &fargs[1];
959959

960+
// Warn when quantifier range is large or unbounded, since SAT solvers
961+
// (the default backend) expand quantifiers into conjunctions/disjunctions
962+
// over every value in the range. For unbounded quantifiers over usize,
963+
// this means 2^64 terms, which will exhaust memory or diverge silently.
964+
warn_large_quantifier_range(gcx, lower_bound, upper_bound, span, &quantifier_kind);
965+
960966
// Create a fresh quantified variable.
961967
let base_name = "kani_quantified_var".to_string();
962968
let mut counter = 0;
@@ -1121,6 +1127,86 @@ fn build_quantifier_predicate(
11211127
/// Get the closure's function pointer expression for use in a function call.
11221128
/// This is the fallback path when direct substitution cannot produce a
11231129
/// side-effect-free predicate expression.
1130+
/// Threshold above which a quantifier range is considered too large for SAT
1131+
/// solvers. SAT-based backends expand quantifiers into one term per value in
1132+
/// the range, so anything beyond this is likely to cause memory exhaustion or
1133+
/// silent divergence. The value 1000 is chosen as a conservative default that
1134+
/// covers most practical quantifier ranges while catching accidental unbounded
1135+
/// or excessively large ranges.
1136+
const QUANTIFIER_RANGE_WARN_THRESHOLD: u64 = 1000;
1137+
1138+
/// Emit a compile-time warning when a quantifier's range is statically known
1139+
/// to be large or unbounded. This catches the common mistake of writing
1140+
/// `forall!(|i| ...)` (which expands over all 2^64 usize values) without
1141+
/// selecting an SMT solver.
1142+
///
1143+
/// Only fires when **both** bounds resolve to compile-time integer constants.
1144+
/// Bounded quantifiers (`forall!(|i in (lo, hi)| ...)`) use `let` bindings
1145+
/// for type coercion, which hides the constants from codegen — so this
1146+
/// currently only detects unbounded quantifiers in practice.
1147+
fn warn_large_quantifier_range(
1148+
gcx: &GotocCtx,
1149+
lower_bound: &Expr,
1150+
upper_bound: &Expr,
1151+
span: Span,
1152+
quantifier_kind: &QuantifierKind,
1153+
) {
1154+
let lo = unwrap_to_constant(gcx, lower_bound, 0);
1155+
let hi = unwrap_to_constant(gcx, upper_bound, 0);
1156+
if let (Some(lo), Some(hi)) = (lo, hi)
1157+
// If hi <= lo the range is empty/inverted; no expansion happens, so no warning needed.
1158+
&& hi > lo
1159+
{
1160+
let range_size = &hi - &lo;
1161+
let threshold = num::BigInt::from(QUANTIFIER_RANGE_WARN_THRESHOLD);
1162+
if range_size > threshold {
1163+
let kind = match quantifier_kind {
1164+
QuantifierKind::ForAll => "forall",
1165+
QuantifierKind::Exists => "exists",
1166+
};
1167+
// usize::MAX - usize::MIN = u64::MAX - 1 on 64-bit targets; treat
1168+
// anything in this ballpark as effectively unbounded.
1169+
let near_max = num::BigInt::from(u64::MAX) - num::BigInt::from(1u64);
1170+
let size_str = if range_size >= near_max {
1171+
"unbounded (~2^64)".to_string()
1172+
} else {
1173+
format!("{range_size}")
1174+
};
1175+
let internal_span = rustc_internal::internal(gcx.tcx, span);
1176+
gcx.tcx.dcx().span_warn(
1177+
internal_span,
1178+
format!(
1179+
"`kani::{kind}` has an {size_str} range; \
1180+
SAT solvers (the default backend) expand quantifiers \
1181+
over every value and may exhaust memory or diverge. \
1182+
Consider adding tighter bounds or using an SMT solver \
1183+
(`#[kani::solver(z3)]` or `--solver z3`)."
1184+
),
1185+
);
1186+
}
1187+
}
1188+
}
1189+
1190+
/// Maximum recursion depth for `unwrap_to_constant` to guard against cycles.
1191+
const MAX_UNWRAP_DEPTH: u8 = 5;
1192+
1193+
/// Unwrap typecasts and symbol references to extract an integer constant.
1194+
fn unwrap_to_constant(gcx: &GotocCtx, expr: &Expr, depth: u8) -> Option<num::BigInt> {
1195+
if depth > MAX_UNWRAP_DEPTH {
1196+
return None;
1197+
}
1198+
expr.int_constant_value().or_else(|| match expr.value() {
1199+
cbmc::goto_program::ExprValue::Typecast(inner) => unwrap_to_constant(gcx, inner, depth + 1),
1200+
cbmc::goto_program::ExprValue::Symbol { identifier } => {
1201+
gcx.symbol_table.lookup(*identifier).and_then(|sym| match &sym.value {
1202+
SymbolValues::Expr(init) => unwrap_to_constant(gcx, init, depth + 1),
1203+
_ => None,
1204+
})
1205+
}
1206+
_ => None,
1207+
})
1208+
}
1209+
11241210
fn find_closure_call_expr(instance: &Instance, gcx: &mut GotocCtx, loc: Location) -> Option<Expr> {
11251211
for arg in instance.args().0.iter() {
11261212
let arg_ty = arg.ty()?;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
`kani::forall` has an unbounded (~2^64) range; SAT solvers (the default backend) expand quantifiers over every value and may exhaust memory or diverge. Consider adding tighter bounds or using an SMT solver
2+
`kani::exists` has an unbounded (~2^64) range; SAT solvers (the default backend) expand quantifiers over every value and may exhaust memory or diverge. Consider adding tighter bounds or using an SMT solver
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
// kani-flags: -Zquantifiers --solver z3
4+
5+
//! Test that Kani emits a compile-time warning when a quantifier has a large
6+
//! statically-known range. Unbounded quantifiers (no explicit bounds) expand to
7+
//! usize::MIN..usize::MAX, which are visible as constants at codegen time.
8+
//! Bounded quantifiers use let bindings for type coercion, so their constants
9+
//! are not visible at codegen time — a known limitation.
10+
//!
11+
//! Note: We use --solver z3 so the harnesses actually verify (SAT would hang
12+
//! on unbounded ranges). The warning fires at codegen time regardless of solver.
13+
14+
#[kani::proof]
15+
fn check_unbounded_forall_warns() {
16+
// Unbounded quantifier expands to usize::MIN..usize::MAX: should warn
17+
assert!(kani::forall!(|i| i < 10 || i >= 10));
18+
}
19+
20+
#[kani::proof]
21+
fn check_unbounded_exists_warns() {
22+
// Unbounded exists also warns
23+
assert!(kani::exists!(|i| i == 42));
24+
}
25+
26+
#[kani::proof]
27+
fn check_small_forall_no_warn() {
28+
// Range of 10 < threshold: should NOT trigger warning.
29+
// (No expected output for this harness — absence of warning is the test.)
30+
assert!(kani::forall!(|i in (0, 10)| i < 10));
31+
}

0 commit comments

Comments
 (0)