Support FnParam for closure-typed function parameters in invariants#169
Conversation
`thrust_models::FnParam<F>` in a loop invariant could not refer to a closure-typed function parameter: the macro rewrote a bare `F` to `Closure<F>` but never descended into the generic arguments of composite path types, so `FnParam<F>` was left as-is and `f.at_entry()` produced `<F as Model>::Ty` instead of the expected `Closure<F>`, causing a type mismatch. `lower_closure_type_params_in_ty` now recurses into a path type's generic arguments (and qualified self), so `FnParam<F>` becomes `FnParam<Closure<F>>` and `f.at_entry()` recovers the modeled closure. A closure with no captures models to a null (singleton) sort. Ordinary singleton-sorted parameters are already collapsed to their canonical value in `build_env_from_params` so they are never referenced by a variable in a formula, but `FnParam<Inner>` wrappers were excluded from that handling because the wrapper type does not `build` to a meaningful type. As a result a singleton closure passed via `FnParam` kept a param-variable reference, which later ICE'd during clause building. Classify the wrapped parameter by its `Inner` sort instead, so a singleton closure collapses to its canonical value like any other singleton, keeping singleton value vars out of refinements. Adds a pass/fail test pair: `last_apply` proves a postcondition from an invariant that relates the accumulator to the entry closure's postcondition via `post!(f.at_entry())` (a capturing closure), and `unchanged` covers the null-sort closure-identity case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y4wjjU3RJ1Nrp82Vrci7Qn
There was a problem hiding this comment.
Pull request overview
This PR fixes closure-typed function parameters referenced via thrust_models::FnParam<F> inside invariants by ensuring closure type parameters are correctly lowered when nested in generic path arguments, and by preventing singleton (null-sort) closures wrapped in FnParam from reaching clause building as variables.
Changes:
- Macro: recurse into
syn::Type::Pathgeneric arguments (andqself) soFnParam<F>becomesFnParam<Closure<F>>during invariant type lowering. - Analyzer: classify
FnParam<Inner>parameters by theirInnertype for singleton detection/collapsing to avoid ICEs when the wrapped closure is singleton-sorted. - Tests: add pass/fail UI coverage for both capturing-closure postcondition reasoning via
f.at_entry()and capture-free (null-sort) closure identity comparisons.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| thrust-macros/src/formula_fn_type_lowering.rs | Extends closure type-param lowering to descend into path generic arguments / qualified self. |
| src/analyze/annot_fn.rs | Treats FnParam<Inner> parameters as Inner for singleton collapsing to prevent ICEs. |
| tests/ui/pass/loop_invariant_fn_param_closure.rs | New passing UI test exercising FnParam<F> with a capturing closure in an invariant. |
| tests/ui/fail/loop_invariant_fn_param_closure.rs | New failing UI test ensuring the scenario is checked under Unsat expectations and avoids ICE. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1865775859
ℹ️ 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".
Problem
Referring to a closure-typed function parameter through
thrust_models::FnParam<F>in a loop invariant failed to compile:The invariant macro rewrites a closure type parameter
FtoClosure<F>, but only for a bareF— it never descended into the generic arguments of composite path types, soFnParam<F>was left untouched andf.at_entry()produced<F as Model>::Tyinstead ofClosure<F>.Changes
macro (
formula_fn_type_lowering.rs):lower_closure_type_params_in_tynow recurses into a path type's generic arguments (and qualified self), soFnParam<F>becomesFnParam<Closure<F>>andf.at_entry()recovers the modeled closure.analyzer (
annot_fn.rs): a capture-free closure models to a null (singleton) sort. Ordinary singleton parameters are already collapsed to their canonical value inbuild_env_from_paramsso they never appear as a variable in a formula, butFnParam<Inner>wrappers were excluded from that handling (the wrapper type does notbuildto a meaningful sort). A singleton closure passed viaFnParamtherefore kept a param-variable reference and later ICE'd during clause building. The wrapped parameter is now classified by itsInnersort, so a singleton closure collapses like any other singleton and no singleton value var reaches a refinement.Tests
Adds a pass/fail pair
loop_invariant_fn_param_closure:last_applyproves a postcondition from an invariant relating the accumulator to the entry closure's postcondition viapost!(f.at_entry())(capturing closure).unchangedcovers the null-sort closure-identity case.Known limitations (not addressed here)
FnMutclosure,post!(f.at_entry()(..))hits a sort mismatch: the closure-contract predicate takes the environment asMut<Tuple>whilef.at_entry()yields a plainTuple.exists(|fn_: Array<Int, Closure<F>>| fn_[0] == f.at_entry())) type-checks and no longer ICEs, but the solver returnsunknownon the existential over an array of closures.🤖 Generated with Claude Code
Generated by Claude Code