comemo_macros/lib.rs
1extern crate proc_macro;
2
3/// Return an error at the given item.
4macro_rules! bail {
5 ($item:expr, $fmt:literal $($tts:tt)*) => {
6 return Err(Error::new_spanned(
7 &$item,
8 format!(concat!("comemo: ", $fmt) $($tts)*)
9 ))
10 }
11}
12
13mod memoize;
14mod track;
15
16use proc_macro::TokenStream as BoundaryStream;
17use proc_macro2::TokenStream;
18use quote::{quote, quote_spanned};
19use syn::spanned::Spanned;
20use syn::{parse_quote, Error, Result};
21
22/// Memoize a function.
23///
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///
30/// - _Hashed:_ This is the default. These arguments are hashed into a
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.
56///
57/// - The output of a memoized function must be `Send` and `Sync` because it is
58/// stored in the global cache.
59///
60/// Furthermore, memoized functions cannot use destructuring patterns in their
61/// arguments.
62///
63/// # Example
64/// ```
65/// /// Evaluate a `.calc` script.
66/// #[comemo::memoize]
67/// fn evaluate(script: &str, files: comemo::Tracked<Files>) -> i32 {
68/// script
69/// .split('+')
70/// .map(str::trim)
71/// .map(|part| match part.strip_prefix("eval ") {
72/// Some(path) => evaluate(&files.read(path), files),
73/// None => part.parse::<i32>().unwrap(),
74/// })
75/// .sum()
76/// }
77/// ```
78///
79#[proc_macro_attribute]
80pub fn memoize(_: BoundaryStream, stream: BoundaryStream) -> BoundaryStream {
81 let func = syn::parse_macro_input!(stream as syn::Item);
82 memoize::expand(&func)
83 .unwrap_or_else(|err| err.to_compile_error())
84 .into()
85}
86
87/// Make a type trackable.
88///
89/// This attribute can be applied to an inherent implementation block or trait
90/// definition. It implements the `Track` trait for the type or trait object.
91///
92/// # Tracking immutably and mutably
93/// This allows you to
94///
95/// - call `track()` on that type, producing a `Tracked<T>` container. Used as
96/// an argument to a memoized function, these containers enjoy fine-grained
97/// access tracking instead of blunt hashing.
98///
99/// - call `track_mut()` on that type, producing a `TrackedMut<T>`. For mutable
100/// arguments, tracking is the only option, so that comemo can replay the side
101/// effects when there is a cache hit.
102///
103/// If you attempt to track any mutable methods, your type must implement
104/// [`Clone`] so that comemo can roll back attempted mutations which did not
105/// result in a cache hit.
106///
107/// # Restrictions
108/// Tracked impl blocks or traits may not be generic and may only contain
109/// methods. Just like with memoized functions, certain restrictions apply to
110/// tracked methods:
111///
112/// - The **only obversable impurity tracked methods may exhibit are mutations
113/// through `&mut self`.** Comemo stops you from using basic mutable arguments
114/// and return values, but it cannot determine all sources of impurity, so
115/// this is your responsibility. Tracked methods also must not return mutable
116/// references or other types which allow untracked mutation. You _are_
117/// allowed to use interior mutability if it is not observable (even in
118/// immutable methods, as long as they stay idempotent).
119///
120/// - The return values of tracked methods must implement
121/// [`Hash`](std::hash::Hash) and **must feed all the information they expose
122/// to the hasher**. Otherwise, memoized results might get reused invalidly.
123///
124/// - The arguments to a tracked method must be `Send` and `Sync` because they
125/// are stored in the global cache.
126///
127/// Furthermore:
128/// - Tracked methods cannot be generic.
129/// - They cannot be `unsafe`, `async` or `const`.
130/// - They must take an `&self` or `&mut self` parameter.
131/// - Their arguments must implement [`ToOwned`].
132/// - Their return values must implement [`Hash`](std::hash::Hash).
133/// - They cannot use destructuring patterns in their arguments.
134///
135/// # Example
136/// ```
137/// /// File storage.
138/// struct Files(HashMap<PathBuf, String>);
139///
140/// #[comemo::track]
141/// impl Files {
142/// /// Load a file from storage.
143/// fn read(&self, path: &str) -> String {
144/// self.0.get(Path::new(path)).cloned().unwrap_or_default()
145/// }
146/// }
147///
148/// impl Files {
149/// /// Write a file to storage.
150/// fn write(&mut self, path: &str, text: &str) {
151/// self.0.insert(path.into(), text.into());
152/// }
153/// }
154/// ```
155#[proc_macro_attribute]
156pub fn track(_: BoundaryStream, stream: BoundaryStream) -> BoundaryStream {
157 let block = syn::parse_macro_input!(stream as syn::Item);
158 track::expand(&block)
159 .unwrap_or_else(|err| err.to_compile_error())
160 .into()
161}