Skip to content

Commit 2bf022d

Browse files
coord-eclaude
andauthored
Let a ghost term name generic- and Self-typed variables (#232)
* Lift a formula into a formula function in one place `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 * Resolve a lifted formula function's receiver parameter in one place 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 * Let a ghost term name generic- and `Self`-typed variables 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
2 parents 10fa12d + 26fc574 commit 2bf022d

12 files changed

Lines changed: 573 additions & 388 deletions

File tree

src/analyze/annot_fn.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@ pub struct FormulaFn<'tcx> {
1919
formula: chc::Formula<rty::FunctionParamIdx>,
2020
}
2121

22+
/// The source name a parameter of a formula function lifted out of a function body
23+
/// (`invariant!`, `ghost!`) refers to.
24+
///
25+
/// The lifted function is free, where `self` is not a legal parameter name, so a formula
26+
/// naming the receiver gets a synthetic parameter instead. It stands for the value that
27+
/// debug info records as `self`.
28+
pub fn lifted_param_source_name(ident: rustc_span::symbol::Ident) -> rustc_span::Symbol {
29+
if ident.name.as_str() == "__thrust_self" {
30+
rustc_span::Symbol::intern("self")
31+
} else {
32+
ident.name
33+
}
34+
}
35+
2236
impl<'a, D> Pretty<'a, D, termcolor::ColorSpec> for &FormulaFn<'_>
2337
where
2438
D: pretty::DocAllocator<'a, termcolor::ColorSpec>,

src/analyze/basic_block.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,9 +1067,10 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> {
10671067
.skip(1)
10681068
.map(|ident| {
10691069
let ident = ident.expect("ghost term parameters must be named");
1070-
let operand = self.operand_of_name(ident.name).unwrap_or_else(|| {
1070+
let name = analyze::annot_fn::lifted_param_source_name(ident);
1071+
let operand = self.operand_of_name(name).unwrap_or_else(|| {
10711072
self.tcx.dcx().fatal(format!(
1072-
"ghost term refers to `{ident}`, which is not a live variable here"
1073+
"ghost term refers to `{name}`, which is not a live variable here"
10731074
))
10741075
});
10751076
self.operand_refined_type(operand)

src/analyze/local_def.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -875,13 +875,7 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> {
875875
.unwrap_or(*input_ty)
876876
};
877877

878-
// The synthetic `__thrust_self` parameter (emitted when an invariant refers to the receiver
879-
// `self`) maps to the loop-carried receiver, which appears as `self` in debug info.
880-
let name = if ident.name.as_str() == "__thrust_self" {
881-
rustc_span::Symbol::intern("self")
882-
} else {
883-
ident.name
884-
};
878+
let name = analyze::annot_fn::lifted_param_source_name(ident);
885879

886880
if input_ty
887881
.ty_adt_def()

tests/ui/fail/ghost_generic.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//@error-in-other-file: Unsat
2+
//@compile-flags: -C debug-assertions=off -A unused-variables
3+
4+
use thrust_models::Ghost;
5+
6+
#[thrust_macros::requires(g == v)]
7+
fn expect_same<T>(g: Ghost<T>, v: T) {
8+
let _ = g;
9+
}
10+
11+
#[thrust_macros::context]
12+
fn record<T: Copy>(a: T, b: T) {
13+
let g = thrust_macros::ghost!(|b: T| -> T { b });
14+
expect_same(g, a);
15+
}
16+
17+
fn main() {
18+
record(3_i64, 5_i64);
19+
}

tests/ui/fail/ghost_self.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//@error-in-other-file: Unsat
2+
//@compile-flags: -C debug-assertions=off -A unused-variables
3+
4+
use thrust_models::model::{Int, Seq};
5+
use thrust_models::Ghost;
6+
7+
struct Counter {
8+
count: i64,
9+
seen: Ghost<Seq<Int>>,
10+
}
11+
12+
impl thrust_models::Model for Counter {
13+
type Ty = (Int, Seq<Int>);
14+
}
15+
16+
#[thrust_macros::context]
17+
impl Counter {
18+
#[thrust_macros::requires((*self).1.len() == (*self).0)]
19+
#[thrust_macros::ensures((!self).1.len() == (!self).0)]
20+
fn record(&mut self, x: i64) {
21+
self.count += 1;
22+
self.seen = thrust_macros::ghost!(|self: &mut Self, x: i64| -> Seq<Int> { (*self).1 });
23+
}
24+
}
25+
26+
fn main() {}

tests/ui/pass/ghost_generic.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//@check-pass
2+
//@compile-flags: -C debug-assertions=off -A unused-variables
3+
4+
use thrust_models::Ghost;
5+
6+
#[thrust_macros::requires(g == v)]
7+
fn expect_same<T>(g: Ghost<T>, v: T) {
8+
let _ = g;
9+
}
10+
11+
#[thrust_macros::context]
12+
fn record<T: Copy>(a: T, b: T) {
13+
let g = thrust_macros::ghost!(|a: T| -> T { a });
14+
expect_same(g, a);
15+
}
16+
17+
fn main() {
18+
record(3_i64, 5_i64);
19+
}

tests/ui/pass/ghost_self.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//@check-pass
2+
//@compile-flags: -C debug-assertions=off -A unused-variables
3+
4+
use thrust_models::model::{Int, Seq};
5+
use thrust_models::Ghost;
6+
7+
struct Counter {
8+
count: i64,
9+
seen: Ghost<Seq<Int>>,
10+
}
11+
12+
impl thrust_models::Model for Counter {
13+
type Ty = (Int, Seq<Int>);
14+
}
15+
16+
#[thrust_macros::context]
17+
impl Counter {
18+
#[thrust_macros::requires((*self).1.len() == (*self).0)]
19+
#[thrust_macros::ensures((!self).1.len() == (!self).0)]
20+
fn record(&mut self, x: i64) {
21+
self.count += 1;
22+
self.seen =
23+
thrust_macros::ghost!(|self: &mut Self, x: i64| -> Seq<Int> { (*self).1.push(x) });
24+
}
25+
}
26+
27+
fn main() {}

thrust-macros/src/context.rs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
//!
33
//! Makes the enclosing context available to the specifications written inside an item.
44
//!
5-
//! On a function, every `thrust_macros::invariant!(...)` in the body is rewritten into
6-
//! its context-carrying counterpart, carrying the host signature and, for a method, the
7-
//! enclosing `impl`/`trait` header, so an invariant may refer to generic- and
8-
//! `Self`-typed variables that the standalone macro cannot see. That also extends the
9-
//! function's where clause with the `Model` predicates for every in-scope type parameter
10-
//! (and for `Self` when used), since each injected marker call instantiates a
11-
//! `Model`-bounded formula function with the host's own generics.
5+
//! On a function, every `thrust_macros::invariant!(...)` and `thrust_macros::ghost!(...)`
6+
//! in the body is rewritten into its context-carrying counterpart, carrying the host
7+
//! signature and, for a method, the enclosing `impl`/`trait` header, so a formula may
8+
//! refer to generic- and `Self`-typed variables that the standalone macros cannot see.
9+
//! That also extends the function's where clause with the `Model` predicates for every
10+
//! in-scope type parameter (and for `Self` when used), since each injected marker call
11+
//! instantiates a `Model`-bounded formula function with the host's own generics.
1212
//!
1313
//! On an `impl`/`trait`, each method is stamped with the enclosing header — which is what
1414
//! method-level `requires`/`ensures` read to recover the outer generics — and with this
@@ -80,9 +80,9 @@ fn expand_outer(mut outer_item: FnOuterItem) -> TokenStream {
8080
outer_item.into_token_stream().into()
8181
}
8282

83-
/// Rewrites each `invariant!` in the body into its context-carrying counterpart and
84-
/// extends the where clause with the `Model` predicates those calls need. A body naming
85-
/// no invariant — or a trait method that has no body at all — is left as it is.
83+
/// Rewrites each spec macro in the body into its context-carrying counterpart and extends
84+
/// the where clause with the `Model` predicates those calls need. A body naming no spec
85+
/// macro — or a trait method that has no body at all — is left as it is.
8686
fn expand_fn(mut func: FnItemWithSignature) -> TokenStream {
8787
let outer = match crate::extract_outer_context(func.attrs()) {
8888
Ok(outer) => outer,
@@ -146,19 +146,25 @@ impl ContextInjector<'_> {
146146

147147
impl VisitMut for ContextInjector<'_> {
148148
fn visit_macro_mut(&mut self, mac: &mut syn::Macro) {
149-
if !is_invariant_macro(&mac.path) {
149+
let Some(with_context) = context_carrying_form(&mac.path) else {
150150
return;
151-
}
151+
};
152152
self.injected = true;
153153
if crate::tokens_contain_ident(&mac.tokens, "Self") {
154154
self.self_used = true;
155155
}
156156
mac.tokens = self.inject_context(&mac.tokens);
157-
mac.path = syn::parse_quote!(::thrust_macros::_invariant_with_context);
157+
mac.path = with_context;
158158
}
159159
}
160160

161-
fn is_invariant_macro(path: &syn::Path) -> bool {
161+
/// The context-carrying counterpart of a spec macro that takes a formula over live
162+
/// variables, or `None` for any other macro.
163+
fn context_carrying_form(path: &syn::Path) -> Option<syn::Path> {
162164
// TODO: identify the macro precisely
163-
path.segments.last().is_some_and(|s| s.ident == "invariant")
165+
match path.segments.last()?.ident.to_string().as_str() {
166+
"invariant" => Some(syn::parse_quote!(::thrust_macros::_invariant_with_context)),
167+
"ghost" => Some(syn::parse_quote!(::thrust_macros::_ghost_with_context)),
168+
_ => None,
169+
}
164170
}

0 commit comments

Comments
 (0)