1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
extern crate proc_macro;
/// Return an error at the given item.
use TokenStream as BoundaryStream;
use TokenStream;
use ;
use Spanned;
use ;
/// Memoize a function.
///
/// This attribute can be applied to free-standing functions as well as methods
/// in inherent and trait impls.
///
/// # Kinds of arguments
/// Memoized functions can take three different kinds of arguments:
///
/// - _Hashed:_ This is the default. These arguments are hashed into a
/// high-quality 128-bit hash, which is used as a cache key.
///
/// - _Immutably tracked:_ The argument is of the form `Tracked<T>`. These
/// arguments enjoy fine-grained access tracking. This allows cache hits to
/// occur even if the value of `T` is different than previously as long as the
/// difference isn't observed.
///
/// - _Mutably tracked:_ The argument is of the form `TrackedMut<T>`. Through
/// this type, you can safely mutate an argument from within a memoized
/// function. If there is a cache hit, comemo will replay all mutations.
/// Mutable tracked methods cannot have return values.
///
/// # Restrictions
/// The following restrictions apply to memoized functions:
///
/// - For the memoization to be correct, the [`Hash`](std::hash::Hash)
/// implementations of your arguments **must feed all the information they
/// expose to the hasher**. Otherwise, memoized results might get reused
/// invalidly.
///
/// - The **only observable impurity memoized functions may exhibit are
/// mutations through `TrackedMut<T>` arguments.** Comemo stops you from using
/// basic mutable arguments, but it cannot determine all sources of impurity,
/// so this is your responsibility.
///
/// - Memoized functions must **call tracked methods in _reorderably
/// deterministic_ fashion.** Consider two executions A and B of a memoized
/// function. We define the following two properties:
///
/// - _In-order deterministic:_ If the first N tracked calls and their results
/// are the same in A and B, then the N+1th call must also be the same. This
/// is a fairly natural property as far as deterministic functions go, as,
/// if the first N calls and their results were the same across two
/// execution, the available information for choosing the N+1th call is the
/// same. However, this property is a bit too restrictive in practice. For
/// instance, a function that internally uses multi-threading may call
/// tracked methods out-of-order while still producing a deterministic
/// result.
///
/// - _Reorderably deterministic:_ If, for the first N calls in A, B has
/// matching calls (same arguments, same return value) somewhere in its call
/// sequence, then the N+1th call invoked by A must also occur _somewhere_
/// in the call sequence of B. This is a somewhat relaxed version of
/// in-order determinism that still allows comemo to perform internal
/// optimizations while permitting memoization of many more functions (e.g.
/// ones that use internal multi-threading in an outwardly deterministic
/// fashion).
///
/// Reorderable determinism is necessary for efficient cache lookups. If a
/// memoized function is not reorderably determinstic, comemo may panic in
/// debug mode to bring your attention to this. Meanwhile, in release mode,
/// memoized functions will still yield correct results, but caching may prove
/// ineffective.
///
/// - The output of a memoized function must be `Send` and `Sync` because it is
/// stored in the global cache.
///
/// Furthermore, memoized functions cannot use destructuring patterns in their
/// arguments.
///
/// # Example
/// ```
/// /// Evaluate a `.calc` script.
/// #[comemo::memoize]
/// fn evaluate(script: &str, files: comemo::Tracked<Files>) -> i32 {
/// script
/// .split('+')
/// .map(str::trim)
/// .map(|part| match part.strip_prefix("eval ") {
/// Some(path) => evaluate(&files.read(path), files),
/// None => part.parse::<i32>().unwrap(),
/// })
/// .sum()
/// }
/// ```
///
/// # Disabling memoization conditionally
/// If you want to enable or disable memoization for a function conditionally,
/// you can use the `enabled` attribute. This is useful for cheap function calls
/// where dealing with the caching is more expensive than recomputing the
/// result. This allows you to bypass hashing and constraint validation while
/// still dealing with the same function signature. And allows saving memory and
/// time.
///
/// By default, all functions are unconditionally memoized. To disable
/// memoization conditionally, you must specify an `enabled = <expr>` attribute.
/// The expression can use the parameters and must evaluate to a boolean value.
/// If the expression is `false`, the function will be executed without hashing
/// and caching.
///
/// ## Example
/// ```
/// /// Compute the sum of a slice of floats, but only memoize if the slice is
/// /// longer than 1024 elements.
/// #[comemo::memoize(enabled = add.len() > 1024)]
/// fn evaluate(add: &[f32]) -> f32 {
/// add.iter().copied().sum()
/// }
/// ```
///
/// Make a type trackable.
///
/// This attribute can be applied to an inherent implementation block or trait
/// definition. It implements the `Track` trait for the type or trait object.
///
/// # Tracking immutably and mutably
/// This allows you to
///
/// - call `track()` on that type, producing a `Tracked<T>` container. Used as
/// an argument to a memoized function, these containers enjoy fine-grained
/// access tracking instead of blunt hashing.
///
/// - call `track_mut()` on that type, producing a `TrackedMut<T>`. For mutable
/// arguments, tracking is the only option, so that comemo can replay the side
/// effects when there is a cache hit.
///
/// # Restrictions
/// Tracked impl blocks or traits may not be generic and may only contain
/// methods. Just like with memoized functions, certain restrictions apply to
/// tracked methods:
///
/// - The **only obversable impurity tracked methods may exhibit are mutations
/// through `&mut self`.** Comemo stops you from using basic mutable arguments
/// and return values, but it cannot determine all sources of impurity, so
/// this is your responsibility. Tracked methods also must not return mutable
/// references or other types which allow untracked mutation. You _are_
/// allowed to use interior mutability if it is not observable (even in
/// immutable methods, as long as they stay idempotent).
///
/// - The return values of tracked methods must implement
/// [`Hash`](std::hash::Hash) and **must feed all the information they expose
/// to the hasher**. Otherwise, memoized results might get reused invalidly.
///
/// - Mutable tracked methods must not have a return value.
///
/// - A tracked implementation cannot have a mix of mutable and immutable
/// methods.
///
/// - The arguments to a tracked method must be `Send` and `Sync` because they
/// are stored in the global cache.
///
/// Furthermore:
/// - Tracked methods cannot be generic.
/// - They cannot be `unsafe`, `async` or `const`.
/// - They must take an `&self` or `&mut self` parameter.
/// - Their arguments must implement [`ToOwned`].
/// - Their return values must implement [`Hash`](std::hash::Hash).
/// - They cannot use destructuring patterns in their arguments.
///
/// # Example
/// ```
/// /// File storage.
/// struct Files(HashMap<PathBuf, String>);
///
/// #[comemo::track]
/// impl Files {
/// /// Load a file from storage.
/// fn read(&self, path: &str) -> String {
/// self.0.get(Path::new(path)).cloned().unwrap_or_default()
/// }
/// }
///
/// impl Files {
/// /// Write a file to storage.
/// fn write(&mut self, path: &str, text: &str) {
/// self.0.insert(path.into(), text.into());
/// }
/// }
/// ```