Skip to content

Commit 36fb31c

Browse files
committed
Mutation tracking
1 parent 9b107f8 commit 36fb31c

File tree

9 files changed

+633
-337
lines changed

9 files changed

+633
-337
lines changed

macros/src/lib.rs

Lines changed: 75 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,47 @@ mod track;
1515

1616
use proc_macro::TokenStream as BoundaryStream;
1717
use proc_macro2::TokenStream;
18-
use quote::{quote, quote_spanned, ToTokens};
18+
use quote::{quote, quote_spanned};
1919
use syn::spanned::Spanned;
2020
use syn::{parse_quote, Error, Result};
2121

2222
/// Memoize a function.
2323
///
24-
/// Memoized functions can take two kinds of arguments:
24+
/// This attribute can be applied to free-standing functions as well as methods
25+
/// in inherent and trait impls.
26+
///
27+
/// # Kinds of arguments
28+
/// Memoized functions can take three different kinds of arguments:
29+
///
2530
/// - _Hashed:_ This is the default. These arguments are hashed into a
26-
/// high-quality 128-bit hash, which is used as a cache key. For this to be
27-
/// correct, the hash implementations of your arguments **must feed all the
28-
/// information your arguments expose to the hasher**. Otherwise, memoized
29-
/// results might get reused invalidly.
30-
/// - _Tracked:_ The argument is of the form `Tracked<T>`. These arguments enjoy
31-
/// fine-grained access tracking and needn't be exactly the same for a cache
32-
/// hit to occur. They only need to be used equivalently.
31+
/// high-quality 128-bit hash, which is used as a cache key.
32+
///
33+
/// - _Immutably tracked:_ The argument is of the form `Tracked<T>`. These
34+
/// arguments enjoy fine-grained access tracking. This allows cache hits to
35+
/// occur even if the value of `T` is different than previously as long as the
36+
/// difference isn't observed.
37+
///
38+
/// - _Mutably tracked:_ The argument is of the form `TrackedMut<T>`. Through
39+
/// this type, you can safely mutate an argument from within a memoized
40+
/// function. If there is a cache hit, comemo will replay all mutations.
41+
/// Mutable tracked methods can also have return values that are tracked just
42+
/// like immutable methods.
43+
///
44+
/// # Restrictions
45+
/// The following restrictions apply to memoized functions:
46+
///
47+
/// - For the memoization to be correct, the [`Hash`](std::hash::Hash)
48+
/// implementations of your arguments **must feed all the information they
49+
/// expose to the hasher**. Otherwise, memoized results might get reused
50+
/// invalidly.
51+
///
52+
/// - The **only obversable impurity memoized functions may exhibit are
53+
/// mutations through `TrackedMut<T>` arguments.** Comemo stops you from using
54+
/// basic mutable arguments, but it cannot determine all sources of impurity,
55+
/// so this is your responsibility.
3356
///
34-
/// You can also add the `#[memoize]` attribute to methods in inherent and trait
35-
/// impls.
57+
/// Furthermore, memoized functions cannot use destructuring patterns in their
58+
/// arguments.
3659
///
3760
/// # Example
3861
/// ```
@@ -50,14 +73,6 @@ use syn::{parse_quote, Error, Result};
5073
/// }
5174
/// ```
5275
///
53-
/// # Restrictions
54-
/// There are certain restrictions that apply to memoized functions. Most of
55-
/// these are checked by comemo, but some are your responsibility:
56-
/// - They must be **pure**, that is, **free of observable side effects**. This
57-
/// is **your responsibility** as comemo can't check it.
58-
/// - They must have an explicit return type.
59-
/// - They cannot have mutable parameters (conflicts with purity).
60-
/// - They cannot use destructuring patterns in their arguments.
6176
#[proc_macro_attribute]
6277
pub fn memoize(_: BoundaryStream, stream: BoundaryStream) -> BoundaryStream {
6378
let func = syn::parse_macro_input!(stream as syn::Item);
@@ -68,13 +83,48 @@ pub fn memoize(_: BoundaryStream, stream: BoundaryStream) -> BoundaryStream {
6883

6984
/// Make a type trackable.
7085
///
71-
/// Adding this to an impl block of a type `T` implements the `Track` trait for
72-
/// `T`. This lets you call `.track()` on that type, producing a `Tracked<T>`.
73-
/// When such a tracked type is used an argument to a memoized function it
74-
/// enjoys fine-grained access tracking instead of being bluntly hashed.
86+
/// This attribute can be applied to an inherent implementation block or trait
87+
/// definition. It implements the `Track` trait for the type or trait object.
88+
///
89+
/// # Tracking immutably and mutably
90+
/// This allows you to
91+
///
92+
/// - call `track()` on that type, producing a `Tracked<T>` container. Used as
93+
/// an argument to a memoized function, these containers enjoy fine-grained
94+
/// access tracking instead of blunt hashing.
95+
///
96+
/// - call `track_mut()` on that type, producing a `TrackedMut<T>`. For mutable
97+
/// arguments, tracking is the only option, so that comemo can replay the side
98+
/// effects when there is a cache hit.
99+
///
100+
/// If you attempt to track any mutable methods, your type must implement
101+
/// [`Clone`] so that comemo can roll back attempted mutations which did not
102+
/// result in a cache hit.
103+
///
104+
/// # Restrictions
105+
/// Tracked impl blocks or traits may not be generic and may only contain
106+
/// methods. Just like with memoized functions, certain restrictions apply to
107+
/// tracked methods:
75108
///
76-
/// You can also add the `#[track]` attribute to a trait to make its trait
77-
/// object trackable.
109+
/// - The **only obversable impurity tracked methods may exhibit are mutations
110+
/// through `&mut self`.** Comemo stops you from using basic mutable arguments
111+
/// and return values, but it cannot determine all sources of impurity, so
112+
/// this is your responsibility. Tracked methods also must not return mutable
113+
/// references or other types which allow untracked mutation. You _are_
114+
/// allowed to use interior mutability if it is not observable (even in
115+
/// immutable methods, as long as they stay idempotent).
116+
///
117+
/// - The return values of tracked methods must implement
118+
/// [`Hash`](std::hash::Hash) and **must feed all the information they expose
119+
/// to the hasher**. Otherwise, memoized results might get reused invalidly.
120+
///
121+
/// Furthermore:
122+
/// - Tracked methods cannot be generic.
123+
/// - They cannot be `unsafe`, `async` or `const`.
124+
/// - They must take an `&self` or `&mut self` parameter.
125+
/// - Their arguments must implement [`ToOwned`](std::borrow::ToOwned).
126+
/// - Their return values must implement [`Hash`](std::hash::Hash).
127+
/// - They cannot use destructuring patterns in their arguments.
78128
///
79129
/// # Example
80130
/// ```
@@ -96,24 +146,6 @@ pub fn memoize(_: BoundaryStream, stream: BoundaryStream) -> BoundaryStream {
96146
/// }
97147
/// }
98148
/// ```
99-
///
100-
/// # Restrictions
101-
/// Tracked impl blocks or traits may not be generic and may only contain
102-
/// methods. Just like with memoized functions, certain restrictions apply to
103-
/// tracked methods:
104-
/// - They must be **pure**, that is, **free of observable side effects**. This
105-
/// is **your responsibility** as comemo can't check it. You can use interior
106-
/// mutability as long as the method stays idempotent.
107-
/// - Their **return values must implement `Hash`** and **must feed all the
108-
/// information they expose to the hasher**. Otherwise, memoized results might
109-
/// get reused invalidly.
110-
/// - They cannot be generic.
111-
/// - They can only be private or public not `pub(...)`.
112-
/// - They cannot be `unsafe`, `async` or `const`.
113-
/// - They must take an `&self` parameter.
114-
/// - They must have an explicit return type.
115-
/// - They cannot have mutable parameters (conflicts with purity).
116-
/// - They cannot use destructuring patterns in their arguments.
117149
#[proc_macro_attribute]
118150
pub fn track(_: BoundaryStream, stream: BoundaryStream) -> BoundaryStream {
119151
let block = syn::parse_macro_input!(stream as syn::Item);

macros/src/memoize.rs

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,8 @@ use super::*;
22

33
/// Memoize a function.
44
pub fn expand(item: &syn::Item) -> Result<proc_macro2::TokenStream> {
5-
let item = match item {
6-
syn::Item::Fn(item) => item,
7-
_ => bail!(
8-
item,
9-
"`memoize` can only be applied to functions and methods"
10-
),
5+
let syn::Item::Fn(item) = item else {
6+
bail!(item, "`memoize` can only be applied to functions and methods");
117
};
128

139
// Preprocess and validate the function.
@@ -26,17 +22,8 @@ struct Function {
2622

2723
/// An argument to a memoized function.
2824
enum Argument {
29-
Ident(syn::Ident),
3025
Receiver(syn::Token![self]),
31-
}
32-
33-
impl ToTokens for Argument {
34-
fn to_tokens(&self, tokens: &mut TokenStream) {
35-
match self {
36-
Self::Ident(ident) => ident.to_tokens(tokens),
37-
Self::Receiver(token) => token.to_tokens(tokens),
38-
}
39-
}
26+
Ident(Option<syn::Token![mut]>, syn::Ident),
4027
}
4128

4229
/// Preprocess and validate a function.
@@ -48,9 +35,7 @@ fn prepare(function: &syn::ItemFn) -> Result<Function> {
4835
}
4936

5037
let output = match &function.sig.output {
51-
syn::ReturnType::Default => {
52-
bail!(function.sig, "memoized function must have a return type")
53-
}
38+
syn::ReturnType::Default => parse_quote! { () },
5439
syn::ReturnType::Type(_, ty) => ty.as_ref().clone(),
5540
};
5641

@@ -68,31 +53,27 @@ fn prepare_arg(input: &syn::FnArg) -> Result<Argument> {
6853
Argument::Receiver(recv.self_token)
6954
}
7055
syn::FnArg::Typed(typed) => {
71-
let name = match typed.pat.as_ref() {
72-
syn::Pat::Ident(syn::PatIdent {
73-
by_ref: None,
74-
mutability: None,
75-
ident,
76-
subpat: None,
77-
..
78-
}) => ident.clone(),
79-
pat => bail!(pat, "only simple identifiers are supported"),
56+
let syn::Pat::Ident(syn::PatIdent {
57+
by_ref: None,
58+
mutability,
59+
ident,
60+
subpat: None,
61+
..
62+
}) = typed.pat.as_ref() else {
63+
bail!(typed.pat, "only simple identifiers are supported");
8064
};
8165

82-
let ty = typed.ty.as_ref().clone();
83-
match ty {
84-
syn::Type::Reference(syn::TypeReference {
85-
mutability: Some(_), ..
86-
}) => {
87-
bail!(
88-
typed.ty,
89-
"memoized functions cannot have mutable parameters"
90-
)
91-
}
92-
_ => {}
66+
if let syn::Type::Reference(syn::TypeReference {
67+
mutability: Some(_), ..
68+
}) = typed.ty.as_ref()
69+
{
70+
bail!(
71+
typed.ty,
72+
"memoized functions cannot have mutable parameters"
73+
)
9374
}
9475

95-
Argument::Ident(name)
76+
Argument::Ident(mutability.clone(), ident.clone())
9677
}
9778
})
9879
}
@@ -101,24 +82,28 @@ fn prepare_arg(input: &syn::FnArg) -> Result<Argument> {
10182
fn process(function: &Function) -> Result<TokenStream> {
10283
// Construct assertions that the arguments fulfill the necessary bounds.
10384
let bounds = function.args.iter().map(|arg| {
85+
let val = match arg {
86+
Argument::Receiver(token) => quote! { #token },
87+
Argument::Ident(_, ident) => quote! { #ident },
88+
};
10489
quote_spanned! { function.item.span() =>
105-
::comemo::internal::assert_hashable_or_trackable(&#arg);
90+
::comemo::internal::assert_hashable_or_trackable(&#val);
10691
}
10792
});
10893

10994
// Construct a tuple from all arguments.
11095
let args = function.args.iter().map(|arg| match arg {
111-
Argument::Ident(id) => id.to_token_stream(),
11296
Argument::Receiver(token) => quote! {
11397
::comemo::internal::hash(&#token)
11498
},
99+
Argument::Ident(_, ident) => quote! { #ident },
115100
});
116101
let arg_tuple = quote! { (#(#args,)*) };
117102

118103
// Construct a tuple for all parameters.
119104
let params = function.args.iter().map(|arg| match arg {
120-
Argument::Ident(id) => id.to_token_stream(),
121105
Argument::Receiver(_) => quote! { _ },
106+
Argument::Ident(mutability, ident) => quote! { #mutability #ident },
122107
});
123108
let param_tuple = quote! { (#(#params,)*) };
124109

@@ -129,8 +114,13 @@ fn process(function: &Function) -> Result<TokenStream> {
129114

130115
// Adjust the function's body.
131116
let mut wrapped = function.item.clone();
132-
let unique = quote! { __ComemoUnique };
117+
for arg in wrapped.sig.inputs.iter_mut() {
118+
let syn::FnArg::Typed(typed) = arg else { continue };
119+
let syn::Pat::Ident(ident) = typed.pat.as_mut() else { continue };
120+
ident.mutability = None;
121+
}
133122

123+
let unique = quote! { __ComemoUnique };
134124
wrapped.block = parse_quote! { {
135125
struct #unique;
136126
#(#bounds;)*

0 commit comments

Comments
 (0)