Skip to content

Bridge the rust-call receiver in pre!/post! - #216

Merged
coord-e merged 4 commits into
mainfrom
claude/issue-206-hajur9
Aug 14, 2026
Merged

Bridge the rust-call receiver in pre!/post!#216
coord-e merged 4 commits into
mainfrom
claude/issue-206-hajur9

Conversation

@coord-e

@coord-e coord-e commented Aug 14, 2026

Copy link
Copy Markdown
Owner

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:

Either way the predicate variable was emitted at a sort other than its declaration, and the solver aborted before producing a verdict:

unknown constant p2 (A1_Tuple<Mut<Int>>)
declared: (declare-fun p2 (A2_Mut<Tuple<Mut<Int>>>) Bool)

Change

closure_receiver_term takes the receiver term through the same three steps RustCallVisitor performs on the argument of a call:

  • a closure value borrowed as &{closure};
  • a closure value borrowed as &mut {closure};
  • a &mut {closure} reborrowed as &{closure}.

A receiver the body already takes as it stands is left alone. In particular a receiver that carries a Mut for a body that takes one keeps it, and with it the upvars the call leaves behind — which is what Mut::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

cargo test is green at 314 UI tests (308 before), as are cargo fmt --check and cargo clippy -D warnings.

The comment on closure_captures_fn_once explaining that pre!/post! strip a FnMut closure's upvars of their Mut no longer describes the behaviour, so it is updated to the reason that still holds.

🤖 Generated with Claude Code

https://claude.ai/code/session_0192XpBfrsKiGya1e3t3Cj84

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_term in AnnotFnTranslator and 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.

Comment thread tmp.LCvQPp73Qu.smt2 Outdated
Comment on lines +1 to +5
(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))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@coord-e
coord-e force-pushed the claude/issue-206-hajur9 branch from a0111cf to ab230d9 Compare August 14, 2026 02:27
claude added 3 commits August 14, 2026 02:51
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
@coord-e
coord-e force-pushed the claude/issue-206-hajur9 branch from e25da32 to 4122e61 Compare August 14, 2026 02:52
@coord-e coord-e changed the title Match the closure receiver to its contract in pre!/post! Match the receiver of pre!/post! to the closure's function type Aug 14, 2026
@coord-e
coord-e requested a lite review from Copilot August 14, 2026 03:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@coord-e
coord-e force-pushed the claude/issue-206-hajur9 branch from bc7cb14 to 4122e61 Compare August 14, 2026 07:43
@coord-e coord-e changed the title Match the receiver of pre!/post! to the closure's function type Bridge the rust-call receiver in pre!/post! Aug 14, 2026
@coord-e
coord-e force-pushed the claude/issue-206-hajur9 branch from 370cb96 to e3ec5b3 Compare August 14, 2026 08:13
@coord-e

coord-e commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: e3ec5b3dea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

@coord-e
coord-e force-pushed the claude/issue-206-hajur9 branch 2 times, most recently from 8edda61 to 43a09ff Compare August 14, 2026 14:23
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
@coord-e
coord-e force-pushed the claude/issue-206-hajur9 branch from 43a09ff to f6f3192 Compare August 14, 2026 14:33
@coord-e
coord-e merged commit cbc7d3e into main Aug 14, 2026
6 checks passed
@coord-e
coord-e deleted the claude/issue-206-hajur9 branch August 14, 2026 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants