Match the receiver of pre!/post! to the closure's function type - #216
Match the receiver of pre!/post! to the closure's function type#216coord-e wants to merge 3 commits into
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
Fixes #206.
Problem
A closure's pre- and postcondition are over its upvars as its body receives them: a closure that mutates its upvars takes them behind a
Mut, 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, so the two could disagree:f: F) against aMut<upvars>parameter — 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 parameter that is the upvars themselves.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_termadapts the receiver term to the first parameter of the closure'srty::FunctionType: a closure value stands for upvars that the call leaves as they are (mut(t, t)), and a&mutto a closure contributes the upvars it holds on entry (mut_current(t)). Receivers that already match — including one built withMut::new— pass through untouched.Pinning the upvars on exit to the ones on entry keeps the system in the Horn fragment, which an existential or a universal over the prophecy would leave. It is the weaker reading, assumed by the callee and proven by the caller through the same term, so the only cost is that a bare
pre!/post!cannot carry upvars whose value changes across calls. Naming the receiver withMut::new(f, g)expresses that, 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 opposite 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