Skip to content

Support Self and generic variables in loop invariants#102

Closed
coord-e wants to merge 11 commits into
mainfrom
claude/vigilant-davinci-RiDYY
Closed

Support Self and generic variables in loop invariants#102
coord-e wants to merge 11 commits into
mainfrom
claude/vigilant-davinci-RiDYY

Conversation

@coord-e
Copy link
Copy Markdown
Owner

@coord-e coord-e commented Jun 4, 2026

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

claude and others added 11 commits June 3, 2026 15:27
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.
@coord-e
Copy link
Copy Markdown
Owner Author

coord-e commented Jun 4, 2026

mistakenly duplicated #99 ......

@coord-e coord-e closed this Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants