diff --git a/src/analyze/annot.rs b/src/analyze/annot.rs index b246df43..76bf5c78 100644 --- a/src/analyze/annot.rs +++ b/src/analyze/annot.rs @@ -233,6 +233,22 @@ pub fn fn_param_at_entry_path() -> [Symbol; 3] { ] } +pub fn fn_param_at_here_path() -> [Symbol; 3] { + [ + Symbol::intern("thrust"), + Symbol::intern("def"), + Symbol::intern("fn_param_at_here"), + ] +} + +pub fn fn_param_is_not_changed_path() -> [Symbol; 3] { + [ + Symbol::intern("thrust"), + Symbol::intern("def"), + Symbol::intern("fn_param_is_not_changed"), + ] +} + pub fn closure_precondition_path() -> [Symbol; 3] { [ Symbol::intern("thrust"), diff --git a/src/analyze/annot_fn.rs b/src/analyze/annot_fn.rs index 029db1d1..35e30dd9 100644 --- a/src/analyze/annot_fn.rs +++ b/src/analyze/annot_fn.rs @@ -264,6 +264,43 @@ impl<'a, 'tcx> AnnotFnTranslator<'a, 'tcx> { .is_some_and(|def| Some(def.did()) == self.def_ids.fn_param_wrapper()) } + /// A `FnParam` invariant parameter denotes the function argument's value at function entry + /// (`OuterFnParam`); `at_entry()` is therefore the identity on the parameter. + fn fn_param_at_entry_term( + &self, + receiver: &'tcx rustc_hir::Expr<'tcx>, + ) -> chc::Term { + self.to_term(receiver) + } + + /// `at_here()` denotes the argument's *current* local at the loop header, which is a distinct + /// basic-block parameter from the entry value. The two are wired to separate parameters in + /// [`crate::analyze::local_def::Analyzer::build_invariant_precondition`]: each invariant + /// parameter `i` has its entry value at variable `i` and its "here" value at variable + /// `params.len() + i`. + fn fn_param_at_here_term( + &self, + receiver: &'tcx rustc_hir::Expr<'tcx>, + ) -> chc::Term { + let chc::Term::Var(idx) = self.to_term(receiver) else { + panic!("FnParam::at_here receiver must be a function-parameter binding"); + }; + chc::Term::var(rty::FunctionParamIdx::from( + self.body.params.len() + idx.index(), + )) + } + + fn fn_param_is_not_changed( + &self, + receiver: &'tcx rustc_hir::Expr<'tcx>, + ) -> FormulaOrTerm { + FormulaOrTerm::BinOp( + self.fn_param_at_entry_term(receiver), + AmbiguousBinOp::Eq, + self.fn_param_at_here_term(receiver), + ) + } + fn build_env_from_pat( &mut self, param: chc::Term, @@ -669,8 +706,21 @@ impl<'a, 'tcx> AnnotFnTranslator<'a, 'tcx> { args.is_empty(), "FnParam::at_entry does not take any arguments" ); - let t = self.to_term(receiver); - return FormulaOrTerm::Term(t); + return FormulaOrTerm::Term(self.fn_param_at_entry_term(receiver)); + } + if Some(def_id) == self.def_ids.fn_param_at_here() { + assert!( + args.is_empty(), + "FnParam::at_here does not take any arguments" + ); + return FormulaOrTerm::Term(self.fn_param_at_here_term(receiver)); + } + if Some(def_id) == self.def_ids.fn_param_is_not_changed() { + assert!( + args.is_empty(), + "FnParam::is_not_changed does not take any arguments" + ); + return self.fn_param_is_not_changed(receiver); } if Some(def_id) == self.def_ids.seq_len() { assert!(args.is_empty(), "Seq::len does not take any arguments"); @@ -759,8 +809,19 @@ impl<'a, 'tcx> AnnotFnTranslator<'a, 'tcx> { } if Some(def_id) == self.def_ids.fn_param_at_entry() { assert_eq!(args.len(), 1, "FnParam::at_entry takes exactly 1 argument"); - let t = self.to_term(&args[0]); - return FormulaOrTerm::Term(t); + return FormulaOrTerm::Term(self.fn_param_at_entry_term(&args[0])); + } + if Some(def_id) == self.def_ids.fn_param_at_here() { + assert_eq!(args.len(), 1, "FnParam::at_here takes exactly 1 argument"); + return FormulaOrTerm::Term(self.fn_param_at_here_term(&args[0])); + } + if Some(def_id) == self.def_ids.fn_param_is_not_changed() { + assert_eq!( + args.len(), + 1, + "FnParam::is_not_changed takes exactly 1 argument" + ); + return self.fn_param_is_not_changed(&args[0]); } if Some(def_id) == self.def_ids.implies() { let [lhs, rhs] = args else { diff --git a/src/analyze/basic_block.rs b/src/analyze/basic_block.rs index 6e5aecf2..835a6623 100644 --- a/src/analyze/basic_block.rs +++ b/src/analyze/basic_block.rs @@ -1305,19 +1305,6 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> { let var = self.env.immut_bind_tmp(param_unrefined_rty); param_terms.insert(param_idx, chc::Term::var(var.into())); outer_fn_param_vars.insert(outer_idx, var); - - // XXX: A user-supplied loop invariant replaces the inferred precondition, so - // it no longer carries the relationship between a function's entry argument - // (`OuterFnParam`) and its current local. The invariant cannot express both - // views of the same argument with the current syntax. As an ad-hoc workaround, - // equate them when the local is non-flow, since its representation cannot change. - let local = analyze::local_of_function_param(outer_idx); - if !param_ty.to_sort().is_singleton() && self.env.is_non_flow_local(local) { - assumption.body.push_conj( - chc::Term::var(PlaceTypeVar::Var(var)) - .equal_to(self.env.local_type(local).term), - ); - } } BasicBlockTypeParamKind::Synthetic => {} } diff --git a/src/analyze/did_cache.rs b/src/analyze/did_cache.rs index 4dbcf110..170f619b 100644 --- a/src/analyze/did_cache.rs +++ b/src/analyze/did_cache.rs @@ -38,6 +38,8 @@ struct DefIds { fn_param_wrapper: OnceCell>, fn_param_at_entry: OnceCell>, + fn_param_at_here: OnceCell>, + fn_param_is_not_changed: OnceCell>, closure_precondition: OnceCell>, closure_postcondition: OnceCell>, @@ -270,6 +272,19 @@ impl<'tcx> DefIdCache<'tcx> { .get_or_init(|| self.annotated_def(&crate::analyze::annot::fn_param_at_entry_path())) } + pub fn fn_param_at_here(&self) -> Option { + *self + .def_ids + .fn_param_at_here + .get_or_init(|| self.annotated_def(&crate::analyze::annot::fn_param_at_here_path())) + } + + pub fn fn_param_is_not_changed(&self) -> Option { + *self.def_ids.fn_param_is_not_changed.get_or_init(|| { + self.annotated_def(&crate::analyze::annot::fn_param_is_not_changed_path()) + }) + } + pub fn closure_precondition(&self) -> Option { *self .def_ids diff --git a/src/analyze/local_def.rs b/src/analyze/local_def.rs index 3938e071..1ee11695 100644 --- a/src/analyze/local_def.rs +++ b/src/analyze/local_def.rs @@ -1009,7 +1009,18 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> { .fn_sig(formula_def_id.to_def_id()) .instantiate(self.tcx, generic_args); - let mut mapping: Vec = Vec::with_capacity(idents.len()); + // Each invariant parameter `i` is substituted by a term over `bty`'s parameters. A + // `FnParam` parameter denotes the argument's value at function entry (`OuterFnParam`); + // its current local at the loop header (`at_here()`) is a *separate* basic-block parameter. + // To expose both from a single invariant parameter without conflating them, variable `i` + // carries the entry value and variable `params.len() + i` carries the "here" value (see + // `AnnotFnTranslator::fn_param_at_here_term`). Ordinary parameters map to their loop-header + // local and have no distinct "here" value. + let free = |idx| chc::Term::var(rty::RefinedTypeVar::Free(idx)); + let mut entry_mapping: Vec>> = + Vec::with_capacity(idents.len()); + let mut here_mapping: Vec>> = + Vec::with_capacity(idents.len()); for (ident_opt, input_ty) in idents.iter().zip(sig.skip_binder().inputs()) { let name = ident_opt.expect("invariant parameters must be named").name; let input_ty = { @@ -1038,21 +1049,44 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> { )) }); let param_idx = crate::analyze::function_param_of_local(local); - mapping.push(bty.param_of_outer_fn_param(param_idx).unwrap()); + let at_entry = free(bty.param_of_outer_fn_param(param_idx).unwrap()); + // `at_here()` is the argument's current local at the loop header. It may have been + // moved or dropped (not live), in which case there is no distinct "here" value and + // we fall back to the entry value. + let at_here = bty + .param_of_local(local) + .map(free) + .unwrap_or_else(|| at_entry.clone()); + entry_mapping.push(at_entry); + here_mapping.push(at_here); } else { + // Referring to a function argument's local without `FnParam` is ambiguous about + // which view (entry vs. current) is meant, so it is rejected. The receiver `self` + // is exempt: it is loop-carried and always denotes its current value. + if name.as_str() != "self" && self.function_param_local_of_name(name).is_some() { + self.tcx.dcx().fatal(format!( + "loop invariant refers to function parameter `{name}` directly; \ + use `{name}: FnParam<..>` with `.at_entry()`/`.at_here()` to choose \ + between its value at function entry and its current value" + )) + } let local = self.local_of_name_in_bb(name, bty).unwrap_or_else(|| { self.tcx.dcx().fatal(format!( "loop invariant refers to `{name}`, which is not a live variable at the loop header" )) }); - mapping.push(bty.param_of_local(local).unwrap()); + let term = free(bty.param_of_local(local).unwrap()); + // Ordinary parameters have no separate "here" value; the slot is never referenced. + entry_mapping.push(term.clone()); + here_mapping.push(term); } } + let mapping: Vec<_> = entry_mapping.into_iter().chain(here_mapping).collect(); formula_fn .formula() .clone() - .subst_var(|idx| chc::Term::var(rty::RefinedTypeVar::Free(mapping[idx.index()]))) + .subst_var(|idx| mapping[idx.index()].clone()) .into() } diff --git a/src/refine/env.rs b/src/refine/env.rs index ba494acb..d9c3dfbc 100644 --- a/src/refine/env.rs +++ b/src/refine/env.rs @@ -922,10 +922,6 @@ where self.locals.contains_key(&local) || self.flow_locals.contains_key(&local) } - pub fn is_non_flow_local(&self, local: Local) -> bool { - self.locals.contains_key(&local) - } - fn flow_binding(&self, var: Var) -> Option<&FlowBinding> { match var { Var::Local(local) => self.flow_locals.get(&local), diff --git a/std.rs b/std.rs index 9d1a883f..bc522321 100644 --- a/std.rs +++ b/std.rs @@ -415,6 +415,20 @@ mod thrust_models { pub fn at_entry(self) -> T { unimplemented!() } + + #[allow(dead_code)] + #[thrust::def::fn_param_at_here] + #[thrust::ignored] + pub fn at_here(self) -> T { + unimplemented!() + } + + #[allow(dead_code)] + #[thrust::def::fn_param_is_not_changed] + #[thrust::ignored] + pub fn is_not_changed(&self) -> bool { + unimplemented!() + } } } diff --git a/tests/ui/fail/loop_invariant_fn_param_at_here.rs b/tests/ui/fail/loop_invariant_fn_param_at_here.rs new file mode 100644 index 00000000..9dc2be8b --- /dev/null +++ b/tests/ui/fail/loop_invariant_fn_param_at_here.rs @@ -0,0 +1,26 @@ +//@error-in-other-file: Unsat +//@compile-flags: -C debug-assertions=off + +// `a` is mutated in the loop, so its current value (`at_here`) diverges from its entry value. +// Claiming `is_not_changed()` (i.e. `at_entry() == at_here()`) is therefore not inductive: it holds +// on entry but is broken by `a += 1`, so verification fails. + +#[thrust_macros::requires(true)] +#[thrust_macros::ensures(true)] +#[thrust::trusted] +fn rand() -> bool { + unimplemented!() +} + +#[thrust_macros::ensures(result == a)] +#[thrust_macros::invariant_context] +fn count_up(mut a: i64) -> i64 { + while rand() { + thrust_macros::invariant!(|a: thrust_models::FnParam| a.is_not_changed()); + a += 1; + } + + a +} + +fn main() {} diff --git a/tests/ui/fail/loop_invariant_fn_param_at_here_mut_borrow.rs b/tests/ui/fail/loop_invariant_fn_param_at_here_mut_borrow.rs new file mode 100644 index 00000000..4f2229c3 --- /dev/null +++ b/tests/ui/fail/loop_invariant_fn_param_at_here_mut_borrow.rs @@ -0,0 +1,27 @@ +//@error-in-other-file: Unsat +//@compile-flags: -C debug-assertions=off + +// `a` is mutably borrowed and incremented through the borrow, so its current value (`at_here`) +// diverges from its entry value. Claiming `is_not_changed()` (i.e. `at_entry() == at_here()`) is +// therefore not inductive: it holds on entry but is broken by `*r += 1`, so verification fails. + +#[thrust_macros::requires(true)] +#[thrust_macros::ensures(true)] +#[thrust::trusted] +fn rand() -> bool { + unimplemented!() +} + +#[thrust_macros::ensures(result == a)] +#[thrust_macros::invariant_context] +fn count_up(mut a: i64) -> i64 { + while rand() { + thrust_macros::invariant!(|a: thrust_models::FnParam| a.is_not_changed()); + let r = &mut a; + *r += 1; + } + + a +} + +fn main() {} diff --git a/tests/ui/fail/loop_invariant_fn_param_direct.rs b/tests/ui/fail/loop_invariant_fn_param_direct.rs new file mode 100644 index 00000000..10e75aa0 --- /dev/null +++ b/tests/ui/fail/loop_invariant_fn_param_direct.rs @@ -0,0 +1,28 @@ +//@error-in-other-file: refers to function parameter `a` directly +//@compile-flags: -C debug-assertions=off + +// Naively referring to a function argument's local directly (without `FnParam`) is rejected: the +// choice between its entry value and its current value would be implicit. Compare with the paired +// pass test, which makes that choice explicit via `FnParam` and `at_entry()`/`at_here()`. + +#[thrust_macros::requires(true)] +#[thrust_macros::ensures(true)] +#[thrust::trusted] +fn rand() -> bool { + unimplemented!() +} + +#[thrust_macros::ensures(result == a)] +#[thrust_macros::invariant_context] +fn keep_argument(a: i64) -> i64 { + let mut v = a; + + while rand() { + thrust_macros::invariant!(|a: i64, v: i64| v == a); + v = a; + } + + v +} + +fn main() {} diff --git a/tests/ui/fail/loop_invariant_generic.rs b/tests/ui/fail/loop_invariant_generic.rs index 5342e970..71d623c8 100644 --- a/tests/ui/fail/loop_invariant_generic.rs +++ b/tests/ui/fail/loop_invariant_generic.rs @@ -10,7 +10,7 @@ fn rand() -> i64 { unimplemented!() } fn keep(v: T) { let mut x = v; while rand() == 0 { - thrust_macros::invariant!(|v: T| v == v); + thrust_macros::invariant!(|v: thrust_models::FnParam| v.is_not_changed()); x = v; } assert!(x == v); diff --git a/tests/ui/fail/loop_invariant_outer_param.rs b/tests/ui/fail/loop_invariant_outer_param.rs index 5543908b..c60b8b5e 100644 --- a/tests/ui/fail/loop_invariant_outer_param.rs +++ b/tests/ui/fail/loop_invariant_outer_param.rs @@ -1,6 +1,10 @@ //@error-in-other-file: Unsat //@compile-flags: -C debug-assertions=off +// Without `is_not_changed()` the invariant only relates `v` to the argument's *current* local +// (`at_here`); nothing ties that back to the entry value, so the `result == a` postcondition (whose +// `a` is the entry value) cannot be discharged. + #[thrust_macros::requires(true)] #[thrust_macros::ensures(true)] #[thrust::trusted] @@ -14,7 +18,7 @@ fn keep_argument(a: i64) -> i64 { let mut v = a; while rand() { - thrust_macros::invariant!(|a: i64, v: i64| true); + thrust_macros::invariant!(|a: thrust_models::FnParam, v: i64| v == a.at_here()); v = a; } diff --git a/tests/ui/pass/loop_invariant_fn_param_at_here.rs b/tests/ui/pass/loop_invariant_fn_param_at_here.rs new file mode 100644 index 00000000..8e7bae61 --- /dev/null +++ b/tests/ui/pass/loop_invariant_fn_param_at_here.rs @@ -0,0 +1,26 @@ +//@check-pass +//@compile-flags: -C debug-assertions=off + +// When the argument is mutable and mutated in the loop body, `at_here()` (its current value) +// genuinely differs from `at_entry()` (its value at function entry). `a` only grows, so the +// invariant `a.at_entry() <= a.at_here()` is inductive and discharges `result >= a` (entry value). + +#[thrust_macros::requires(true)] +#[thrust_macros::ensures(true)] +#[thrust::trusted] +fn rand() -> bool { + unimplemented!() +} + +#[thrust_macros::ensures(result >= a)] +#[thrust_macros::invariant_context] +fn count_up(mut a: i64) -> i64 { + while rand() { + thrust_macros::invariant!(|a: thrust_models::FnParam| a.at_entry() <= a.at_here()); + a += 1; + } + + a +} + +fn main() {} diff --git a/tests/ui/pass/loop_invariant_fn_param_at_here_mut_borrow.rs b/tests/ui/pass/loop_invariant_fn_param_at_here_mut_borrow.rs new file mode 100644 index 00000000..bf633aac --- /dev/null +++ b/tests/ui/pass/loop_invariant_fn_param_at_here_mut_borrow.rs @@ -0,0 +1,27 @@ +//@check-pass +//@compile-flags: -C debug-assertions=off + +// `a` is mutably borrowed in the loop body, making it flow-bound; its current value (`at_here`) +// then genuinely differs from its value at function entry (`at_entry`). `a` only grows, so the +// invariant `a.at_entry() <= a.at_here()` is inductive and discharges `result >= a` (entry value). + +#[thrust_macros::requires(true)] +#[thrust_macros::ensures(true)] +#[thrust::trusted] +fn rand() -> bool { + unimplemented!() +} + +#[thrust_macros::ensures(result >= a)] +#[thrust_macros::invariant_context] +fn count_up(mut a: i64) -> i64 { + while rand() { + thrust_macros::invariant!(|a: thrust_models::FnParam| a.at_entry() <= a.at_here()); + let r = &mut a; + *r += 1; + } + + a +} + +fn main() {} diff --git a/tests/ui/pass/loop_invariant_fn_param_direct.rs b/tests/ui/pass/loop_invariant_fn_param_direct.rs new file mode 100644 index 00000000..b1ae1be4 --- /dev/null +++ b/tests/ui/pass/loop_invariant_fn_param_direct.rs @@ -0,0 +1,30 @@ +//@check-pass +//@compile-flags: -C debug-assertions=off + +// The paired pass for `fail/loop_invariant_fn_param_direct`: the same program with the function +// argument referred to explicitly via `FnParam`. `is_not_changed()` ties the entry and current +// views together, so `v == a.at_entry()` discharges the `result == a` postcondition. + +#[thrust_macros::requires(true)] +#[thrust_macros::ensures(true)] +#[thrust::trusted] +fn rand() -> bool { + unimplemented!() +} + +#[thrust_macros::ensures(result == a)] +#[thrust_macros::invariant_context] +fn keep_argument(a: i64) -> i64 { + let mut v = a; + + while rand() { + thrust_macros::invariant!( + |a: thrust_models::FnParam, v: i64| a.is_not_changed() && v == a.at_entry() + ); + v = a; + } + + v +} + +fn main() {} diff --git a/tests/ui/pass/loop_invariant_generic.rs b/tests/ui/pass/loop_invariant_generic.rs index 7e31b002..9291de61 100644 --- a/tests/ui/pass/loop_invariant_generic.rs +++ b/tests/ui/pass/loop_invariant_generic.rs @@ -10,7 +10,7 @@ fn rand() -> i64 { unimplemented!() } fn keep(v: T) { let mut x = v; while rand() == 0 { - thrust_macros::invariant!(|x: T, v: T| x == v); + thrust_macros::invariant!(|x: T, v: thrust_models::FnParam| x == v.at_here()); x = v; } assert!(x == v); diff --git a/tests/ui/pass/loop_invariant_generic_closure.rs b/tests/ui/pass/loop_invariant_generic_closure.rs index 73668ebe..555fbd3d 100644 --- a/tests/ui/pass/loop_invariant_generic_closure.rs +++ b/tests/ui/pass/loop_invariant_generic_closure.rs @@ -14,7 +14,7 @@ fn keep i64, T: Copy + PartialEq>(f: F, v: T) { let _ = f; let mut x = v; while rand() == 0 { - thrust_macros::invariant!(|x: T, v: T| x == v); + thrust_macros::invariant!(|x: T, v: thrust_models::FnParam| x == v.at_here()); x = v; } assert!(x == v); diff --git a/tests/ui/pass/loop_invariant_outer_param.rs b/tests/ui/pass/loop_invariant_outer_param.rs index 03838b2f..cb682fda 100644 --- a/tests/ui/pass/loop_invariant_outer_param.rs +++ b/tests/ui/pass/loop_invariant_outer_param.rs @@ -1,6 +1,11 @@ //@check-pass //@compile-flags: -C debug-assertions=off +// A user-supplied loop invariant replaces the inferred precondition, so it must itself relate the +// argument's value at function entry (`at_entry`, the `OuterFnParam`) to its current local +// (`at_here`). Here `a` is never reassigned, so `is_not_changed()` ties the two together and lets +// `v == a.at_here()` discharge the `result == a` postcondition (whose `a` is the entry value). + #[thrust_macros::requires(true)] #[thrust_macros::ensures(true)] #[thrust::trusted] @@ -14,7 +19,9 @@ fn keep_argument(a: i64) -> i64 { let mut v = a; while rand() { - thrust_macros::invariant!(|a: i64, v: i64| v == a); + thrust_macros::invariant!( + |a: thrust_models::FnParam, v: i64| a.is_not_changed() && v == a.at_here() + ); v = a; }