From f5ba9ad4a53547bbdd604830f6a31b7061fbc075 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 22:09:59 +0000 Subject: [PATCH 1/3] Analyze defs with trait-bounded type params per instantiation A def whose signature does not mention its type parameters was checked once with every type parameter replaced by i32, and that result was reused for all instantiations. Bounds inherited from the parent impl or trait (impl, the trait's Self: K) escaped the guard in placeholder_generic_args, which only looked at the def's own predicates, so the body was verified against i32's impl. Collect constrained type parameters including parent predicates, and defer such defs so they are analyzed per instantiation. Fixes #330 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013nBCcj2NXQuLQiJoxndHyP --- src/analyze/crate_.rs | 18 ++++++++++--- tests/ui/fail/generic_impl_static_method.rs | 30 +++++++++++++++++++++ tests/ui/pass/generic_impl_static_method.rs | 30 +++++++++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 tests/ui/fail/generic_impl_static_method.rs create mode 100644 tests/ui/pass/generic_impl_static_method.rs diff --git a/src/analyze/crate_.rs b/src/analyze/crate_.rs index fe0a3271..76dd7648 100644 --- a/src/analyze/crate_.rs +++ b/src/analyze/crate_.rs @@ -114,6 +114,7 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> { #[tracing::instrument(skip(self), fields(def_id = %self.tcx.def_path_str(local_def_id)))] fn refine_fn_def(&mut self, local_def_id: LocalDefId) { let sig = self.ctx.fn_sig(local_def_id.to_def_id()); + let has_constrained_type_param = !self.constrained_type_params(local_def_id).is_empty(); let mut analyzer = self.ctx.local_def_analyzer(local_def_id); @@ -147,7 +148,9 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> { }; use mir_ty::TypeVisitableExt as _; - if sig.has_param() { + // A trait-bounded type parameter must stay generic even when the signature does not + // mention it, since the body depends on which impl it is instantiated with. + if sig.has_param() || has_constrained_type_param { // TODO: needs clear criteria on whether extern_spec'ed target fn is analyzed or not if target_def_id.as_local().is_none_or(|def_id| { self.skip_analysis.contains(&def_id) || !self.tcx.is_mir_available(def_id) @@ -196,11 +199,13 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> { } } - fn placeholder_generic_args(&self, local_def_id: LocalDefId) -> mir_ty::GenericArgsRef<'tcx> { + /// Indices of the type parameters bound by a trait other than `Sized`, including bounds + /// inherited from the parent impl or trait. + fn constrained_type_params(&self, local_def_id: LocalDefId) -> HashSet { let mut constrained_params = HashSet::new(); - let predicates = self.tcx.predicates_of(local_def_id); + let predicates = self.tcx.predicates_of(local_def_id).instantiate_identity(self.tcx); let sized_trait = self.tcx.lang_items().sized_trait().unwrap(); - for (clause, _) in predicates.predicates { + for clause in predicates.predicates { let mir_ty::ClauseKind::Trait(pred) = clause.kind().skip_binder() else { continue; }; @@ -217,6 +222,11 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> { constrained_params.insert(param_ty.index); } } + constrained_params + } + + fn placeholder_generic_args(&self, local_def_id: LocalDefId) -> mir_ty::GenericArgsRef<'tcx> { + let constrained_params = self.constrained_type_params(local_def_id); let mut args: Vec> = Vec::new(); diff --git a/tests/ui/fail/generic_impl_static_method.rs b/tests/ui/fail/generic_impl_static_method.rs new file mode 100644 index 00000000..a47ceec5 --- /dev/null +++ b/tests/ui/fail/generic_impl_static_method.rs @@ -0,0 +1,30 @@ +//@error-in-other-file: Unsat +//@compile-flags: -C debug-assertions=off + +trait K { + fn k() -> i64; +} + +impl K for i32 { + fn k() -> i64 { + 0 + } +} + +impl K for i64 { + fn k() -> i64 { + 1 + } +} + +struct W(T); + +impl W { + fn kk() -> i64 { + T::k() + } +} + +fn main() { + assert!(W::::kk() == 0); +} diff --git a/tests/ui/pass/generic_impl_static_method.rs b/tests/ui/pass/generic_impl_static_method.rs new file mode 100644 index 00000000..dd2c65b6 --- /dev/null +++ b/tests/ui/pass/generic_impl_static_method.rs @@ -0,0 +1,30 @@ +//@check-pass +//@compile-flags: -C debug-assertions=off + +trait K { + fn k() -> i64; +} + +impl K for i32 { + fn k() -> i64 { + 0 + } +} + +impl K for i64 { + fn k() -> i64 { + 1 + } +} + +struct W(T); + +impl W { + fn kk() -> i64 { + T::k() + } +} + +fn main() { + assert!(W::::kk() == 1); +} From 822e825781dc62d6427cb4dbf6d1d20da27a3de8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 22:09:59 +0000 Subject: [PATCH 2/3] Drop an Fn closure called through FnOnce::call_once after the call RustCallVisitor turns the moved closure into an immutable borrow, but moved_locals had already removed its drop obligation, so the prophecies of the mutable borrows it captured were never resolved. Restore the obligation as the FnMut case already does. Fixes #331 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013nBCcj2NXQuLQiJoxndHyP --- src/analyze/basic_block/visitor/rust_call.rs | 5 +++++ tests/ui/fail/closure_fn_once_fn_kind.rs | 15 +++++++++++++++ tests/ui/pass/closure_fn_once_fn_kind.rs | 15 +++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 tests/ui/fail/closure_fn_once_fn_kind.rs create mode 100644 tests/ui/pass/closure_fn_once_fn_kind.rs diff --git a/src/analyze/basic_block/visitor/rust_call.rs b/src/analyze/basic_block/visitor/rust_call.rs index b46c0467..191f5b14 100644 --- a/src/analyze/basic_block/visitor/rust_call.rs +++ b/src/analyze/basic_block/visitor/rust_call.rs @@ -108,6 +108,11 @@ impl<'a, 'tcx, 'ctx> mir::visit::MutVisitor<'tcx> for RustCallVisitor<'a, 'tcx, let borrowed_closure_local = self.insert_immut_borrow(arg_closure_place, arg_closure_ty); args[0].node = mir::Operand::Copy(borrowed_closure_local.into()); + // As in case 3, the closure is no longer consumed by the call, so its + // drop obligation, which `moved_locals` stole, must be restored. + if arg_closure_place.projection.is_empty() { + self.analyzer.drop_after_terminator(arg_closure_place.local); + } tracing::debug!("applied immut-borrow for closure argument"); } } else if matches!( diff --git a/tests/ui/fail/closure_fn_once_fn_kind.rs b/tests/ui/fail/closure_fn_once_fn_kind.rs new file mode 100644 index 00000000..520bf3f3 --- /dev/null +++ b/tests/ui/fail/closure_fn_once_fn_kind.rs @@ -0,0 +1,15 @@ +//@error-in-other-file: Unsat +//@compile-flags: -C debug-assertions=off + +fn call_once i64>(f: F) -> i64 { + f() +} + +fn main() { + let mut x = 1; + let r = &mut x; + *r = 5; + let c = move || *r; + call_once(c); + assert!(x == 6); +} diff --git a/tests/ui/pass/closure_fn_once_fn_kind.rs b/tests/ui/pass/closure_fn_once_fn_kind.rs new file mode 100644 index 00000000..6f54f630 --- /dev/null +++ b/tests/ui/pass/closure_fn_once_fn_kind.rs @@ -0,0 +1,15 @@ +//@check-pass +//@compile-flags: -C debug-assertions=off + +fn call_once i64>(f: F) -> i64 { + f() +} + +fn main() { + let mut x = 1; + let r = &mut x; + *r = 5; + let c = move || *r; + call_once(c); + assert!(x == 5); +} From 6af5f0390a8695b60c4e3893796cc9eafd779eb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 02:12:56 +0000 Subject: [PATCH 3/3] Format crate_.rs Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013nBCcj2NXQuLQiJoxndHyP --- src/analyze/crate_.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/analyze/crate_.rs b/src/analyze/crate_.rs index 76dd7648..c12ae6f1 100644 --- a/src/analyze/crate_.rs +++ b/src/analyze/crate_.rs @@ -203,7 +203,10 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> { /// inherited from the parent impl or trait. fn constrained_type_params(&self, local_def_id: LocalDefId) -> HashSet { let mut constrained_params = HashSet::new(); - let predicates = self.tcx.predicates_of(local_def_id).instantiate_identity(self.tcx); + let predicates = self + .tcx + .predicates_of(local_def_id) + .instantiate_identity(self.tcx); let sized_trait = self.tcx.lang_items().sized_trait().unwrap(); for clause in predicates.predicates { let mir_ty::ClauseKind::Trait(pred) = clause.kind().skip_binder() else {