Support Self and generic variables in loop invariants#99
Open
coord-e wants to merge 12 commits into
Open
Conversation
2 tasks
6e84fab to
c4e64ef
Compare
f8dae72 to
5903aba
Compare
c4e64ef to
f6f063e
Compare
5903aba to
4da71aa
Compare
f6f063e to
61e2146
Compare
4da71aa to
abd3602
Compare
61e2146 to
3d93645
Compare
abd3602 to
0605e6b
Compare
3d93645 to
faaaa7c
Compare
0605e6b to
995ff18
Compare
faaaa7c to
ca7f820
Compare
995ff18 to
35176d6
Compare
4d56d1c to
95a1a9f
Compare
Add `#[thrust_macros::invariant_context]` — a new attribute that threads the surrounding generic context (and, in methods, `Self`) into every `thrust_macros::invariant!(...)` inside the annotated function, impl, or trait. Each call is replaced in-place by an expanded `#[thrust::formula_fn]` plus a marker call, with the threaded generics declared on the formula function and instantiated via turbofish. In methods, `Self` is re-declared as a synthetic type parameter and instantiated with the real `Self` (legal in expression position), so an invariant may refer to generic- and `Self`-typed variables that the standalone `invariant!` macro cannot see. `thrust_macros::context` and the standalone `thrust_macros::invariant!` macro are unchanged; `invariant_context` rewrites the macro tokens before `invariant!` would otherwise expand, so the simple `invariant!` path keeps working as-is for concrete invariants. Adds pass/fail UI test pairs for the generic and `Self` cases. Note that a struct used as an invariant variable must model structurally (its `Model::Ty` must mirror its fields, e.g. a one-field tuple struct models as a one-tuple); a mismatching model trips an existing subtyping panic unrelated to this change. https://claude.ai/code/session_01WB28auaD8dSQrckqBwJWBt
…nd it Previously invariant_context re-implemented the whole invariant! expansion (closure parsing, formula_fn synthesis, marker call), duplicating the logic already in invariant.rs. Now the responsibilities are split, mirroring the existing requires/ensures _outer_context convention: - invariant_context only *injects* the enclosing context into each invariant!(CLOSURE) call, rewriting it into a context-carrying form built from existing syntax (a fn header carrying the in-scope generics/where clause, tagged for methods with #[thrust::_outer_context(impl/trait header)]). It still extends the host fn's where clause with the Model/PartialEq bounds, which a function-like macro cannot do itself. - invariant! now performs the single, shared expansion for both the standalone concrete form and the threaded form, re-declaring the threaded generics (and synthetic Self) on the formula function and instantiating via turbofish. No new bracket syntax is introduced; the context rides on ordinary Rust fn header syntax and the existing _outer_context attribute.
Rather than having invariant! accept two different input shapes (a bare
closure, or a context-carrying fn header), introduce a dedicated internal
macro thrust_macros::_invariant_with_context! for the threaded form.
- invariant! now only accepts a bare predicate closure.
- _invariant_with_context! only accepts the context-carrying fn header.
- invariant_context rewrites each invariant!(CLOSURE) into
_invariant_with_context!(<context fn> { CLOSURE }) by swapping the macro
path as well as injecting the context tokens.
Both macros share Invariant::expand, so there is still a single expansion
implementation. The leading underscore follows the existing _requires_ensures
convention for macros emitted by other macros and not written by hand.
Replace the Invariant struct, which represented a standalone invariant as empty param/where vecs plus is_method=false, with an explicit Option<&Context> argument to a shared expand_invariant function. None now unambiguously means "no threaded context" (standalone invariant!), distinct from a contextful invariant that merely has no generics.
invariant.rs and spec.rs each had their own copy of the _outer_context attribute reader. Hoist the shared attribute-reading logic into the crate root (alongside FnOuterItem and the other shared helpers); spec's wrapper now calls it and keeps only its method-specific mentions_self diagnostic.
invariant_context previously walked impl/trait blocks itself, duplicating the method-iteration that #[thrust_macros::context] already does. Restrict it to a single function: a method recovers its enclosing impl/trait context from the #[thrust::_outer_context(..)] attribute stamped by #[thrust_macros::context], the same mechanism requires/ensures use. Update the loop_invariant_self tests to combine #[context] on the impl with #[invariant_context] on the method accordingly.
The [T: Model, <T as Model>::Ty: PartialEq] predicate pair was built by hand in three places (twice in spec, once in invariant_context). Hoist it into a shared crate-root helper model_predicates(ty); each caller keeps its own which-types-to-bound logic but no longer open-codes the predicate pair.
spec skips Fn/FnMut/FnOnce-bounded type params when adding Model predicates
(closures have no Model instance), but the invariant path bounded every type
param unconditionally. A generic invariant inside a function with a closure-
typed generic param therefore generated F: Model and failed at the call site
with "the trait bound {closure}: Model is not satisfied".
Share spec's Fn-bound check as a crate-root helper has_fn_bound and apply it
in both the formula function (invariant.rs) and the host where clause
(invariant_context.rs). Add a regression test.
…batim Address three points: 1. Reuse the whole model_where_predicates (not just has_fn_bound). Move it to the crate root, retargeted at &Signature, and call it from both the formula function (invariant) and the host where clause (invariant_context). Each path adds Self handling on top (synthetic param vs Self: Model) but no longer open-codes generic collection or the Fn-bound exclusion. 2. Rename InvariantInjector to ContextInjector. 3. Change the _invariant_with_context input to '#outer_attr #signature; #closure'. ContextInjector now pastes the host function signature verbatim, and the expansion parses it back, so model_where_predicates sees the real signature (its generics, where clause, and parameter/return projections).
Resolution of the rebase onto latest main: main (#101) extracted FnOuterItem into its own fn_outer_item module and dropped it from the crate root. Adapt the loop-invariant work accordingly — reference crate::fn_outer_item::FnOuterItem and keep the shared helpers (extract_outer_context, has_fn_bound, model_predicates, model_where_predicates, tokens_contain_ident) in the crate root.
d9b6042 to
bd80c7c
Compare
There was a problem hiding this comment.
Pull request overview
This PR extends loop invariant support in thrust-macros so thrust_macros::invariant! can reference generic-typed and Self-typed variables by threading the enclosing signature/outer context into each invariant expansion.
Changes:
- Adds
#[thrust_macros::invariant_context]plus an internal_invariant_with_context!macro to carry the host signature (and outer impl/trait header) intoinvariant!calls. - Updates the invariant expansion to (optionally) re-declare/instantiate in-scope generics via turbofish, and introduces helper utilities moved into
lib.rs. - Adds UI pass/fail tests covering generic and
Selfinvariant variables (including a closure-typed generic param case).
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| thrust-macros/src/spec.rs | Refactors to use shared crate::model_where_predicates / extract_outer_context helpers. |
| thrust-macros/src/lib.rs | Adds _invariant_with_context proc-macro, invariant_context attribute entrypoint, and shared helper functions. |
| thrust-macros/src/invariant.rs | Extends invariant expansion to accept threaded context and instantiate generics/Self. |
| thrust-macros/src/invariant_context.rs | New attribute macro to rewrite invariant! calls and add required Model bounds to the host function. |
| tests/ui/pass/loop_invariant_self.rs | New passing UI test for Self-typed invariant variables in a method. |
| tests/ui/pass/loop_invariant_generic.rs | New passing UI test for generic-typed invariant variables. |
| tests/ui/pass/loop_invariant_generic_closure.rs | New passing UI test ensuring closure-typed generics aren’t forced to implement Model. |
| tests/ui/fail/loop_invariant_self.rs | New failing UI test for an unsatisfiable Self-typed invariant. |
| tests/ui/fail/loop_invariant_generic.rs | New failing UI test for an unsatisfiable generic-typed invariant. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+20
to
+22
| pub fn expand(item: TokenStream) -> TokenStream { | ||
| let mut item_fn = syn::parse_macro_input!(item as syn::ItemFn); | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Extends
thrust_macros::invariant!to invariants over generic- andSelf-typed variables, by having#[thrust_macros::invariant_context]thread the enclosing context into eachinvariant!.Frontend (
thrust-macros)The work is split so that
invariant_contextonly injects context andinvariant!does all the expansion — and the context rides on ordinary Rust syntax rather than any new bracket grammar.#[thrust_macros::invariant_context]accepts free functions, impl blocks, and trait definitions. It does not expand the invariants itself: for eachinvariant!(CLOSURE)it finds, it rewrites the macro input into a context-carrying form, and extends the host function's where clause with theModel/PartialEqbounds (which a function-like macro cannot add to its caller). The carried context reuses existing syntax — the enclosing function header (generics + where clause) is copied in as afnheader, andSelf/outer generics ride on the existing#[thrust::_outer_context(impl/trait header)]attribute already used by#[thrust_macros::context]forrequires/ensures:invariant!performs the single, shared expansion for both the standalone concrete form and the threaded form. It re-declares the threaded generics (shadowing the enclosing ones) on the generated#[thrust::formula_fn]and instantiates it via turbofish; in methods,Selfis re-declared as a synthetic type parameter and instantiated with the realSelf(legal in expression position).The concrete, non-generic form from Implement loop invariant annotation #95 keeps working without
invariant_context.The macro crate is organized into
context,invariant,invariant_context, andspecmodules; the root keeps only the proc-macro entry points and a few shared private helpers. The closure-parsing /formula_fn-synthesis / marker-call logic lives in exactly one place (invariant).Test plan
Selfcases (loop_invariant_generic,loop_invariant_self).cargo test --test ui,cargo clippy --all-targets,cargo fmt --checkall clean.Notes
Model::Tymust mirror its fields (e.g. a one-field tuple struct models as a one-tuple). A mismatching model trips an existing subtyping panic unrelated to this PR.https://claude.ai/code/session_01E4qhJALh9SAabEL1pQr6na