Bridge the rust-call receiver in pre!/post! - #216
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes an SMT sort-mismatch in pre!/post! for closures by adapting the “receiver term” passed to a closure’s contract so it matches the contract’s environment parameter (e.g., by-value vs &mut-named closures, and Fn vs FnMut capture shapes). This prevents malformed CHC/SMT output that previously caused the solver to abort before returning a verification verdict.
Changes:
- Add
closure_receiver_terminAnnotFnTranslatorand use it for closure pre/postcondition translation to reconcile receiver shape with the closure contract environment parameter. - Add UI pass/fail test pairs covering the reported mismatch cases and the
Mut::new“environment between calls” modeling case. - Update an existing UI test comment to reflect the new closure capture/contract behavior.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/analyze/annot_fn.rs |
Introduces receiver-term adaptation logic and wires it into closure pre/postcondition translation. |
tests/ui/pass/closure_mut_capture_pre_post.rs |
Pass test reproducing the mutating-capture by-value HOF pre!/post! case from #206. |
tests/ui/fail/closure_mut_capture_pre_post.rs |
Fail counterpart asserting an incorrect postcondition for the same scenario. |
tests/ui/pass/closure_ref_mut_pre_post.rs |
Pass test for the opposite mismatch: Fn closure referenced via &mut in pre!/post!. |
tests/ui/fail/closure_ref_mut_pre_post.rs |
Fail counterpart with an incorrect assertion. |
tests/ui/pass/closure_receiver_mut_model_byval.rs |
Pass test pinning the intended modeling that Mut::new can carry environment across calls for by-value closures. |
tests/ui/fail/closure_receiver_mut_model_byval.rs |
Fail counterpart with an incorrect assertion. |
tests/ui/pass/closure_captures_fn_once.rs |
Updates a comment describing why the closure is kept FnOnce and what changes when it becomes FnMut. |
tmp.LCvQPp73Qu.smt2 |
Appears to be an unintended, locally generated solver artifact added to the repo. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| (set-logic HORN) | ||
|
|
||
| (declare-datatypes ((A0_Mut<Int> 0) (A1_Mut<Mut<Int>> 0) (A3_Mut<Tuple<Mut<Int>>> 0) (A4_Tuple<Int> 0) (A2_Tuple<Mut<Int>> 0)) ( | ||
| (par () ( | ||
| (mut<Int> (mut_current<Int> Int) (mut_final<Int> Int)) |
There was a problem hiding this comment.
Right, that file was never meant to be here — removed.
It comes from tests/thrust-pcsat-wrapper, which mktemp -p . a copy of the query and deletes it through a trap on exit. A solver run that gets killed on timeout never runs the trap, so the copy was left behind in the working tree and I swept it up. No fixture is needed; the tests generate their own queries.
Generated by Claude Code
a0111cf to
ab230d9
Compare
A closure's pre- and postcondition are over its upvars as its body receives them, so a closure that mutates them takes them behind a `Mut` while one that only reads them takes the upvars themselves. `pre!(f(..))`/`post!(f(..), r)` applied them to whatever shape the specification names the closure by: a closure value gave the bare upvars, and a `&mut` to a closure gave a `Mut` where the upvars themselves are expected. Either mismatch emitted a predicate variable at a sort other than its declaration, which the solver rejects before producing a verdict. Adapt the receiver term to the first parameter of the closure's function type: a closure value stands for upvars the call leaves as they are, and a `&mut` to a closure contributes the upvars it holds on entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0192XpBfrsKiGya1e3t3Cj84
Naming the closure by value in `post!` leaves its environment as the call found it, so it cannot carry the environment from one call to the next. Building the receiver with `Mut::new` names that environment, which works for a closure held by value as much as for one held behind a `&mut`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0192XpBfrsKiGya1e3t3Cj84
`upvars` is what the rest of the codebase calls the values a closure carries, from `tupled_upvars_ty` down to the existing closure tests, and it does not collide with the translator's own variable environment. The closure's pre- and postcondition live in its `rty::FunctionType`, which names the same thing the surrounding code already reaches for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0192XpBfrsKiGya1e3t3Cj84
e25da32 to
4122e61
Compare
bc7cb14 to
4122e61
Compare
370cb96 to
e3ec5b3
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep it up! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
8edda61 to
43a09ff
Compare
Comparing whether each side is a `Mut` conflates the question with what the
answer is for: the specification names the closure by what it holds, the body
takes it as its rust-call receiver, and the difference is closed by the same
three steps `RustCallVisitor` performs on the argument of a call. A closure
value is borrowed as `&{closure}` or as `&mut {closure}`, and a
`&mut {closure}` is reborrowed as `&{closure}`. Nothing is done otherwise: a
receiver that already carries a `Mut` for a body that takes one keeps it, and
with it the upvars the call leaves behind, which the previous shape spent on a
`Mut` rebuilt from the entry upvars alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192XpBfrsKiGya1e3t3Cj84
43a09ff to
f6f3192
Compare
Fixes #206.
Problem
A specification holds a closure as its signature says, while the closure body takes it as its rust-call receiver: a closure that mutates its upvars takes them behind a
Mut, one that only reads them takes them behind a&, one that consumes them takes them as they are.pre!(f(..))/post!(f(..), r)applied the closure's pre- and postcondition to the receiver as the specification names it, so the two could disagree:f: F) against a body taking&mut {closure}— the mutating-capture case in Ill-typed SMT: a closure that mutates a captured variable, passed to a generic HOF spec'd withpre!/post!, has its pre/postcondition predicate declared at the FnMut receiver sortMut<env>but applied at the bareenvsort, so verification aborts with a solver sort mismatch #206;&mutto a closure (f: &mut F) against a body taking&{closure}.Either way the predicate variable was emitted at a sort other than its declaration, and the solver aborted before producing a verdict:
Change
closure_receiver_termtakes the receiver term through the same three stepsRustCallVisitorperforms on the argument of a call:&{closure};&mut {closure};&mut {closure}reborrowed as&{closure}.A receiver the body already takes as it stands is left alone. In particular a receiver that carries a
Mutfor a body that takes one keeps it, and with it the upvars the call leaves behind — which is whatMut::new(f, g)is for.The mutable borrow of a closure value ends the call holding the upvars it started with. That is as much as a specification naming the closure by value can say about them, and it keeps the system in the Horn fragment, which an existential or a universal over the prophecy would leave. A specification that needs the upvars a call leaves behind names the receiver with
Mut::new(f, g), for a closure held by value as much as for one held behind a&mut; the added test pins this down.Tests
closure_mut_capture_pre_post: the reproduction from Ill-typed SMT: a closure that mutates a captured variable, passed to a generic HOF spec'd withpre!/post!, has its pre/postcondition predicate declared at the FnMut receiver sortMut<env>but applied at the bareenvsort, so verification aborts with a solver sort mismatch #206, aFnMutclosure mutating a capture through a by-value HOF.closure_ref_mut_pre_post: the other mismatch, aFnclosure named through&mut F.closure_receiver_mut_model_byval: two calls to amoveclosure whose upvars hold the counter, with the upvars between the calls named byMut::new.cargo testis green at 314 UI tests (308 before), as arecargo fmt --checkandcargo clippy -D warnings.The comment on
closure_captures_fn_onceexplaining thatpre!/post!strip aFnMutclosure's upvars of theirMutno longer describes the behaviour, so it is updated to the reason that still holds.🤖 Generated with Claude Code
https://claude.ai/code/session_0192XpBfrsKiGya1e3t3Cj84