Skip to main content

lru_cache_macros/
lib.rs

1//! lru-cache-macros
2//! ================
3//!
4//! An attribute procedural macro to automatically cache the result of a function given a set of inputs.
5//!
6//! **This crate has been deprecated in favor of [cache-macro](https://crates.io/crates/cache-macro) which maintains
7//! the same functionality, but supports more than just lru caches. New code should use that crate instead.**
8//!
9//! # Example:
10//!
11//! ```rust
12//! use lru_cache_macros::lru_cache;
13//!
14//! #[lru_cache(20)]
15//! fn fib(x: u32) -> u64 {
16//!     println!("{:?}", x);
17//!     if x <= 1 {
18//!         1
19//!     } else {
20//!         fib(x - 1) + fib(x - 2)
21//!     }
22//! }
23//!
24//! assert_eq!(fib(19), 6765);
25//! ```
26//!
27//! The above example only calls `fib` twenty times, with the values from 0 to 19. All intermediate
28//! results because of the recursion hit the cache.
29//!
30//! # Usage:
31//!
32//! Simply place `#[lru_cache([size])]` above your function. The function must obey a few properties
33//! to use lru_cache:
34//!
35//! * All arguments and return values must implement `Clone`.
36//! * The function may not take `self` in any form.
37//!
38//! The macro will use the LruCache at `::lru_cache::LruCache` by default. This can be changed by
39//! setting the `cache_type` config variable as shown in the configuration section.
40//!
41//! The `LruCache` type used must accept two generic parameters `<Args, Return>` and must support methods
42//! `get_mut(&K)` and `insert(K, V)`. The `lru-cache` crate meets these requirements.
43//!
44//! Currently, this crate only works on nightly rust. However, once the 2018 edition stabilizes as well as the
45//! procedural macro diagnostic interface, it should be able to run on stable.
46//!
47//! # Configuration:
48//!
49//! The lru_cache macro can be configured by adding additional attributes under `#[lru_cache(size)]`.
50//!
51//! All configuration attributes take the form `#[lru_config(...)]`. The available attributes are:
52//!
53//! * `#[lru_config(cache_type = ...)]`
54//!
55//! This allows the cache type used internally to be changed. The default is equivalent to
56//!
57//! ```#[lru_config(cache_type = ::lru_cache::LruCache)]```
58//!
59//! * `#[lru_config(ignore_args = ...)]`
60//!
61//! This allows certain arguments to be ignored for the purposes of caching. That means they are not part of the
62//! hash table key and thus should never influence the output of the function. It can be useful for diagnostic settings,
63//! returning the number of times executed, or other introspection purposes.
64//!
65//! `ignore_args` takes a comma-separated list of variable identifiers to ignore.
66//!
67//! ### Example:
68//! ```rust
69//! use lru_cache_macros::lru_cache;
70//! #[lru_cache(20)]
71//! #[lru_config(ignore_args = call_count)]
72//! fn fib(x: u64, call_count: &mut u32) -> u64 {
73//!     *call_count += 1;
74//!     if x <= 1 {
75//!         1
76//!     } else {
77//!         fib(x - 1, call_count) + fib(x - 2, call_count)
78//!     }
79//! }
80//!
81//! let mut call_count = 0;
82//! assert_eq!(fib(39, &mut call_count), 102_334_155);
83//! assert_eq!(call_count, 40);
84//! ```
85//!
86//! The `call_count` argument can vary, caching is only done based on `x`.
87//!
88//! * `#[lru_config(thread_local)]`
89//!
90//! Store the cache in thread-local storage instead of global static storage. This avoids the overhead of Mutex locking,
91//! but each thread will be given its own cache, and all caching will not affect any other thread.
92//!
93//! Expanding on the first example:
94//!
95//! ```rust
96//! use lru_cache_macros::lru_cache;
97//!
98//! #[lru_cache(20)]
99//! #[lru_config(thread_local)]
100//! fn fib(x: u32) -> u64 {
101//!     println!("{:?}", x);
102//!     if x <= 1 {
103//!         1
104//!     } else {
105//!         fib(x - 1) + fib(x - 2)
106//!     }
107//! }
108//!
109//! assert_eq!(fib(19), 6765);
110//! ```
111//!
112//! # Details
113//! The created cache is stored as a static variable protected by a mutex unless the `#[lru_config(thread_local)]` configuration
114//! is added.
115//!
116//! With the default settings, the fibonacci example will generate the following code:
117//!
118//! ```rust
119//! fn __lru_base_fib(x: u32) -> u64 {
120//!     if x <= 1 { 1 } else { fib(x - 1) + fib(x - 2) }
121//! }
122//! fn fib(x: u32) -> u64 {
123//!     use lazy_static::lazy_static;
124//!     use std::sync::Mutex;
125//!
126//!     lazy_static! {
127//!         static ref cache: Mutex<::lru_cache::LruCache<(u32,), u64>> =
128//!             Mutex::new(::lru_cache::LruCache::new(20usize));
129//!     }
130//!
131//!     let cloned_args = (x.clone(),);
132//!     let mut cache_unlocked = cache.lock().unwrap();
133//!     let stored_result = cache_unlocked.get_mut(&cloned_args);
134//!     if let Some(stored_result) = stored_result {
135//!         return stored_result.clone();
136//!     };
137//!     drop(cache_unlocked);
138//!     let ret = __lru_base_fib(x);
139//!     let mut cache_unlocked = cache.lock().unwrap();
140//!     cache_unlocked.insert(cloned_args, ret.clone());
141//!     ret
142//! }
143//!
144//! ```
145//!
146//! Whereas, if you use the `#[lru_config(thread_local)]` the generated code will look like:
147//!
148//!
149//! ```rust
150//! fn __lru_base_fib(x: u32) -> u64 {
151//!     if x <= 1 { 1 } else { fib(x - 1) + fib(x - 2) }
152//! }
153//! fn fib(x: u32) -> u64 {
154//!     use std::cell::UnsafeCell;
155//!     use std::thread_local;
156//!
157//!     thread_local!(
158//!          static cache: UnsafeCell<::lru_cache::LruCache<(u32,), u64>> =
159//!              UnsafeCell::new(::lru_cache::LruCache::new(20usize));
160//!     );
161//!
162//!     cache.with(|c|
163//!         {
164//!             let mut cache_ref = unsafe { &mut *c.get() };
165//!             let cloned_args = (x.clone(),);
166//!             let stored_result = cache_ref.get_mut(&cloned_args);
167//!             if let Some(stored_result) = stored_result {
168//!                 stored_result.clone()
169//!             } else {
170//!                 let ret = __lru_base_fib(x);
171//!                 cache_ref.insert(cloned_args, ret.clone());
172//!                 ret
173//!             }
174//!         })
175//! }
176//! ```
177
178#![feature(extern_crate_item_prelude)]
179#![feature(proc_macro_diagnostic)]
180#![recursion_limit="128"]
181extern crate proc_macro;
182
183use std::result;
184
185use proc_macro::TokenStream;
186use syn;
187use syn::{Token, parse_quote};
188use syn::spanned::Spanned;
189use syn::punctuated::Punctuated;
190use quote::quote;
191use proc_macro2;
192
193mod config;
194mod error;
195
196use self::error::{DiagnosticError, Result};
197
198// Function shim to allow us to use `Result` and the `?` operator.
199#[proc_macro_attribute]
200pub fn lru_cache(attr: TokenStream, item: TokenStream) -> TokenStream {
201    match lru_cache_impl(attr, item.clone()) {
202        Ok(tokens) => return tokens,
203        Err(e) => {
204            e.emit();
205            return item;
206        }
207    }
208}
209
210// The main entry point for the macro.
211fn lru_cache_impl(attr: TokenStream, item: TokenStream) -> Result<TokenStream> {
212    let mut original_fn: syn::ItemFn = match syn::parse(item.clone()) {
213        Ok(ast) => ast,
214        Err(e) => {
215            let diag = proc_macro2::Span::call_site().unstable()
216                .error("lru_cache may only be used on functions");
217            return Err(DiagnosticError::new_with_syn_error(diag, e));
218        }
219    };
220
221    let (macro_config, out_attributes) =
222        {
223            let attribs = &original_fn.attrs[..];
224            config::Config::parse_from_attributes(attribs)?
225        };
226    original_fn.attrs = out_attributes;
227
228    let mut new_fn = original_fn.clone();
229
230    let cache_size = get_lru_size(attr)?;
231    let return_type = get_cache_fn_return_type(&original_fn)?;
232
233    let new_name = format!("__lru_base_{}", original_fn.ident.to_string());
234    original_fn.ident = syn::Ident::new(&new_name[..], original_fn.ident.span());
235
236    let (call_args, types, cache_args) = get_args_and_types(&original_fn, &macro_config)?;
237    let cloned_args = make_cloned_args_tuple(&cache_args);
238    let fn_path = path_from_ident(original_fn.ident.clone());
239
240    let fn_call = syn::ExprCall {
241        attrs: Vec::new(),
242        paren_token: syn::token::Paren { span: proc_macro2::Span::call_site() },
243        args: call_args.clone(),
244        func: Box::new(fn_path)
245    };
246
247    let tuple_type = syn::TypeTuple {
248        paren_token: syn::token::Paren { span: proc_macro2::Span::call_site() },
249        elems: types,
250    };
251
252    // Build all common types needed for each body impl.
253    let cache_type = &macro_config.cache_type;
254    let cache_type_with_generics: syn::Type = parse_quote! {
255        #cache_type<#tuple_type, #return_type>
256    };
257    let cache_new: syn::Expr = parse_quote! {
258        #cache_type::new(#cache_size)
259    };
260
261    let lru_body = build_cache_body(&cache_type_with_generics, &cache_new, &cloned_args,
262        &fn_call, &macro_config);
263
264
265    new_fn.block = Box::new(lru_body);
266
267    let out = quote! {
268        #original_fn
269
270        #new_fn
271    };
272    Ok(out.into())
273}
274
275// Build the body of the caching function. What is constructed depends on the config value.
276fn build_cache_body(full_cache_type: &syn::Type, cache_new: &syn::Expr,
277                    cloned_args: &syn::ExprTuple, inner_fn_call: &syn::ExprCall,
278                    config: &config::Config) -> syn::Block
279{
280    if config.use_tls {
281        build_tls_cache_body(full_cache_type, cache_new, cloned_args, inner_fn_call)
282    } else {
283        build_mutex_cache_body(full_cache_type, cache_new, cloned_args, inner_fn_call)
284    }
285}
286
287// Build the body of the caching function which puts the cache in thread-local storage.
288fn build_tls_cache_body(full_cache_type: &syn::Type, cache_new: &syn::Expr,
289                     cloned_args: &syn::ExprTuple, inner_fn_call: &syn::ExprCall) -> syn::Block
290{
291    parse_quote! {
292        {
293            use std::cell::UnsafeCell;
294            use std::thread_local;
295            thread_local!(
296                // We use `UnsafeCell` here to allow recursion. Since it is in the TLS, it should
297                // not introduce any actual unsafety.
298                static cache: UnsafeCell<#full_cache_type> =
299                    UnsafeCell::new(#cache_new);
300            );
301            cache.with(|c| {
302                let mut cache_ref = unsafe { &mut *c.get() };
303                let cloned_args = #cloned_args;
304
305                let stored_result = cache_ref.get_mut(&cloned_args);
306                if let Some(stored_result) = stored_result {
307                    stored_result.clone()
308                } else {
309                    let ret = #inner_fn_call;
310                    cache_ref.insert(cloned_args, ret.clone());
311                    ret
312                }
313            })
314        }
315    }
316}
317
318// Build the body of the caching function which guards the static cache with a mutex.
319fn build_mutex_cache_body(full_cache_type: &syn::Type, cache_new: &syn::Expr,
320                     cloned_args: &syn::ExprTuple, inner_fn_call: &syn::ExprCall) -> syn::Block
321{
322    parse_quote! {
323        {
324            use lazy_static::lazy_static;
325            use std::sync::Mutex;
326
327            lazy_static! {
328                static ref cache: Mutex<#full_cache_type> =
329                    Mutex::new(#cache_new);
330            }
331
332            let cloned_args = #cloned_args;
333
334            let mut cache_unlocked = cache.lock().unwrap();
335            let stored_result = cache_unlocked.get_mut(&cloned_args);
336            if let Some(stored_result) = stored_result {
337                return stored_result.clone();
338            };
339
340            // must unlock here to allow potentially recursive call
341            drop(cache_unlocked);
342
343            let ret = #inner_fn_call;
344            let mut cache_unlocked = cache.lock().unwrap();
345            cache_unlocked.insert(cloned_args, ret.clone());
346            ret
347        }
348    }
349}
350
351fn get_cache_fn_return_type(original_fn: &syn::ItemFn) -> Result<Box<syn::Type>> {
352    if let syn::ReturnType::Type(_, ref ty) = original_fn.decl.output {
353        Ok(ty.clone())
354    } else {
355        let diag = original_fn.ident.span().unstable()
356            .error("There's no point of caching the output of a function that has no output");
357        return Err(DiagnosticError::new(diag));
358    }
359}
360
361fn path_from_ident(ident: syn::Ident) -> syn::Expr {
362    let mut segments: Punctuated<_, Token![::]> = Punctuated::new();
363    segments.push(syn::PathSegment { ident: ident, arguments: syn::PathArguments::None });
364    syn::Expr::Path(syn::ExprPath { attrs: Vec::new(), qself: None, path: syn::Path { leading_colon: None, segments: segments} })
365}
366
367fn get_lru_size(attr: TokenStream) -> Result<usize> {
368    let value: result::Result<syn::LitInt, _> = syn::parse(attr.clone());
369
370    if let Ok(val) = value {
371        Ok(val.value() as usize)
372    } else {
373        let diag = proc_macro2::Span::call_site().unstable()
374            .error("The lru_cache macro must specify a maximum cache size as an argument");
375        Err(DiagnosticError::new_with_syn_error(diag, value.err().unwrap()))
376    }
377}
378
379fn make_cloned_args_tuple(args: &Punctuated<syn::Expr, Token![,]>) -> syn::ExprTuple {
380    let mut cloned_args = Punctuated::<_, Token![,]>::new();
381    for arg in args {
382        let call = syn::ExprMethodCall {
383            attrs: Vec::new(),
384            receiver: Box::new(arg.clone()),
385            dot_token: syn::token::Dot { spans: [arg.span(); 1] },
386            method: syn::Ident::new("clone", proc_macro2::Span::call_site()),
387            turbofish: None,
388            paren_token: syn::token::Paren { span: proc_macro2::Span::call_site() },
389            args: Punctuated::new(),
390        };
391        cloned_args.push(syn::Expr::MethodCall(call));
392    }
393    syn::ExprTuple {
394        attrs: Vec::new(),
395        paren_token: syn::token::Paren { span: proc_macro2::Span::call_site() },
396        elems: cloned_args,
397    }
398}
399
400fn get_args_and_types(f: &syn::ItemFn, config: &config::Config) ->
401        Result<(Punctuated<syn::Expr, Token![,]>, Punctuated<syn::Type, Token![,]>, Punctuated<syn::Expr, Token![,]>)>
402{
403    let mut call_args = Punctuated::<_, Token![,]>::new();
404    let mut types = Punctuated::<_, Token![,]>::new();
405    let mut cache_args = Punctuated::<_, Token![,]>::new();
406
407    for input in &f.decl.inputs {
408        match input {
409            syn::FnArg::SelfValue(p) => {
410                let diag = p.span().unstable()
411                    .error("`self` arguments are currently unsupported by lru_cache");
412                return Err(DiagnosticError::new(diag));
413            }
414            syn::FnArg::SelfRef(p) => {
415                let diag = p.span().unstable()
416                    .error("`&self` arguments are currently unsupported by lru_cache");
417                return Err(DiagnosticError::new(diag));
418            }
419            syn::FnArg::Captured(arg_captured) => {
420                let mut segments: syn::punctuated::Punctuated<_, Token![::]> = syn::punctuated::Punctuated::new();
421                let arg_name;
422                if let syn::Pat::Ident(ref pat_ident) = arg_captured.pat {
423                    arg_name = pat_ident.ident.clone();
424                    if let Some(m) = pat_ident.mutability {
425                        if !config.ignore_args.contains(&arg_name) {
426                            let diag = m.span.unstable()
427                                .error("`mut` arguments are not supported with lru_cache as this could lead to incorrect results being stored");
428                            return Err(DiagnosticError::new(diag));
429                        }
430                    }
431                    segments.push(syn::PathSegment { ident: pat_ident.ident.clone(), arguments: syn::PathArguments::None });
432                } else {
433                    let diag = arg_captured.span().unstable()
434                        .error("unsupported argument kind");
435                    return Err(DiagnosticError::new(diag));
436                }
437
438                let arg_path = syn::Expr::Path(syn::ExprPath { attrs: Vec::new(), qself: None, path: syn::Path { leading_colon: None, segments } });
439
440                if !config.ignore_args.contains(&arg_name) {
441
442                    // If the arg type is a reference, remove the reference because the arg will be cloned
443                    if let syn::Type::Reference(type_reference) = &arg_captured.ty {
444                        types.push(type_reference.elem.as_ref().to_owned()); // as_ref -> to_owned unboxes the type
445                    } else {
446                        types.push(arg_captured.ty.clone());
447                    }
448
449                    cache_args.push(arg_path.clone());
450                }
451
452
453                call_args.push(arg_path);
454            },
455            syn::FnArg::Inferred(p) => {
456                let diag = p.span().unstable()
457                    .error("inferred arguments are currently unsupported by lru_cache");
458                return Err(DiagnosticError::new(diag));
459            }
460            syn::FnArg::Ignored(p) => {
461                let diag = p.span().unstable()
462                    .error("ignored arguments are currently unsupported by lru_cache");
463                return Err(DiagnosticError::new(diag));
464            }
465        }
466    }
467
468    if types.len() == 1 {
469        types.push_punct(syn::token::Comma { spans: [proc_macro2::Span::call_site(); 1] })
470    }
471
472    Ok((call_args, types, cache_args))
473}