Skip to content

Let a ghost term name generic- and Self-typed variables - #232

Draft
coord-e wants to merge 3 commits into
mainfrom
claude/generic-parameter-self-reference-8ak4yh
Draft

Let a ghost term name generic- and Self-typed variables#232
coord-e wants to merge 3 commits into
mainfrom
claude/generic-parameter-self-reference-8ak4yh

Conversation

@coord-e

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

Copy link
Copy Markdown
Owner

Follows #220, on top of #231. Three commits, each standing on its own:

4154ba3 move the formula-function lifting invariant! carried into formula_fn_lifting — no behaviour change
eeb87fb name the rule that resolves a lifted function's receiver parameter, and read it from both the invariant and the ghost path
2d34fe7 thread the enclosing context into ghost!, plus tests

The gap

A ghost term is lifted into a free #[thrust::formula_fn], which inherits neither the enclosing function's generics nor Self, so a term could only name variables of concrete type — the last of #220's known gaps. #[thrust_macros::context] already threads that context into invariant!; it now threads it into ghost! too.

#[thrust_macros::context]
impl Counter {
    #[thrust_macros::requires((*self).1.len() == (*self).0)]
    #[thrust_macros::ensures((!self).1.len() == (!self).0)]
    fn record(&mut self, x: i64) {
        self.count += 1;
        self.seen = thrust_macros::ghost!(|self: &mut Self, x: i64| -> Seq<Int> { (*self).1.push(x) });
    }
}
#[thrust_macros::context]
fn record<T: Copy>(a: T, b: T) {
    let g = thrust_macros::ghost!(|a: T| -> T { a });
    expect_same(g, a);
}

How it works

formula_fn_lifting holds what both macros need to lift a formula out of a body:

  • the in-scope generics are re-declared on the formula function and instantiated via turbofish;
  • Self is rewritten to the impl's self type, or to a synthetic type parameter instantiated with the real Self in a trait;
  • the receiver self is renamed to a __thrust_self parameter, which the analyzer resolves back through annot_fn::lifted_param_source_name.

For ghost! the introduced value is parameter 0, so it passes through that lifting as an ordinary parameter: a value type naming Self or a generic is rewritten along with the rest, while the __ghost_marker::<_, T> turbofish keeps the type as written, since the marker call stays in the host's scope.

Tests

ghost_self ghost field of a struct updated by a method whose term names self
ghost_generic ghost value of a generic type, from a term naming a generic-typed live variable

Each as a pass/fail pair; in both, the fail file differs only in which live variable the term names.

Known gaps

  • Self in a generic or trait impl is unsupported, unchanged from invariant! — both now go through the same TODO, so a fix covers them together.

@coord-e
coord-e force-pushed the claude/generic-parameter-self-reference-8ak4yh branch from 149a0ea to 86fd516 Compare August 20, 2026 06:35
@coord-e
coord-e force-pushed the claude/generic-parameter-self-reference-8ak4yh branch from 86fd516 to f981e67 Compare August 20, 2026 11:49
Base automatically changed from claude/unify-context-attribute-8ak4yh to main August 20, 2026 11:54
@coord-e
coord-e force-pushed the claude/generic-parameter-self-reference-8ak4yh branch from f981e67 to 0878392 Compare August 20, 2026 11:54
claude added 3 commits August 20, 2026 12:00
`invariant!` builds its `#[thrust::formula_fn]` from a closure and the context the
closure was written in: the in-scope generics are re-declared on the function and
instantiated via turbofish, `Self` becomes the impl's self type or a synthetic type
parameter in a trait, and the receiver `self` becomes a `__thrust_self` parameter.
None of that is particular to an invariant -- it is what any formula written inside a
function body needs to survive being lifted out of it.

Move it to `formula_fn_lifting`, which takes the parameters and the body and returns
the item plus the expression naming it, and leave `invariant` with the part that is
its own: reading the closure and emitting the marker call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJ6XNNsSBdPkAzrWHftvqV
A formula lifted out of a function body becomes a free function, where `self` is not a
legal parameter name, so one naming the receiver arrives under a synthetic name that
stands for the value debug info records as `self`. The loop-invariant path knew that
rule inline; the ghost path did not know it at all, and looked up the synthetic name as
if a variable of that name were live.

Name the rule and have both paths read the parameter through it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJ6XNNsSBdPkAzrWHftvqV
A ghost term is lifted into a free `#[thrust::formula_fn]`, which inherits neither the
enclosing function's generics nor `Self`, so a term could only name variables of
concrete type. `#[thrust_macros::context]` already threads that context into
`invariant!`; thread it into `ghost!` too, through the same lifting:

    #[thrust_macros::context]
    impl Counter {
        fn record(&mut self, x: i64) {
            self.count += 1;
            self.seen = thrust_macros::ghost!(
                |self: &mut Self, x: i64| -> Seq<Int> { (*self).1.push(x) }
            );
        }
    }

The introduced value is parameter `0` of the lifted function, so it passes through as
an ordinary parameter: a value type naming `Self` or a generic is rewritten along with
the rest, while the `__ghost_marker::<_, T>` turbofish keeps the type as written, the
marker call being in the host's own scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJ6XNNsSBdPkAzrWHftvqV
@coord-e
coord-e force-pushed the claude/generic-parameter-self-reference-8ak4yh branch from 0878392 to 2d34fe7 Compare August 20, 2026 12:03
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