├── .github └── workflows │ └── test.yml ├── .gitignore ├── .hgignore ├── .hgtags ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── ahash.rs ├── custom_hasher.rs ├── empty.rs ├── full_featured.rs ├── fxhash.rs ├── ignore.rs ├── issue30.rs ├── issue32.rs ├── patterns.rs ├── recursive.rs ├── shared.rs └── trivial.rs ├── inner ├── Cargo.toml └── src │ └── lib.rs └── src └── lib.rs /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | build_and_test: 7 | name: Rust project 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | features: ["--features=full", "--no-default-features"] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: stable 17 | - uses: actions-rs/cargo@v1 18 | with: 19 | command: test 20 | args: ${{ matrix.features }} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | ^target/ 2 | glob:Cargo.lock 3 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | 46dbc34d5e199be41143bb198470b606c074fc62 v0.1.3 2 | 45b59350976fb8fa5d7b3b872ea3ff983f0eca62 v0.1.4 3 | b73c35db6c4b71f9fe1e6fc2fec9df5df778aff3 v0.1.5 4 | d701ff3e25d9a1d31f28b46e5d0f224957b4f2cf v0.1.6 5 | d713ed3952ac16f6af929f9cbd924f60ceb298e2 v0.1.7 6 | 853675bcf6abc57f3c55b6a39546ff0330d5a4ff v0.1.8 7 | 9c7a2ca7c2fcf572ea552cbff428d05cdcd4e41e v0.1.9 8 | ff66cf5c7f8c61430db0024cda93c052617ba3e6 v0.3.0 9 | 2ccee100cd3380cb94eca949d55e506c109912bf v0.3.2 10 | 92972ccef3a038648e5b73d4923709e73d4d8f7d v0.3.3 11 | f0d6ef116542d63cc4d376db94b8254a4e83abe8 v0.4.0 12 | 793f2e38a3187ae19d52aaf45a85f5d9b88e8265 v0.4.1 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "memoize" 3 | version = "0.5.1" 4 | description = "Attribute macro for auto-memoizing functions with somewhat-simple signatures" 5 | keywords = ["memoization", "cache", "proc-macro"] 6 | authors = ["Lewin Bormann "] 7 | homepage = "https://github.com/dermesser/memoize" 8 | repository = "https://github.com/dermesser/memoize" 9 | documentation = "https://docs.rs/memoize" 10 | edition = "2018" 11 | license = "MIT" 12 | 13 | [dependencies] 14 | 15 | memoize-inner = { path = "inner/", version = "0.5" } 16 | lazy_static = "1.4" 17 | lru = { version = "0.7", optional = true } 18 | 19 | [dev-dependencies] 20 | 21 | rustc-hash = "1.1.0" 22 | ahash = "0.8.2" 23 | 24 | [workspace] 25 | members = ["inner/"] 26 | 27 | [features] 28 | default = ["full"] 29 | full = ["lru", "memoize-inner/full"] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present The memoize Developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # memoize 2 | 3 | [![Docs.rs](https://docs.rs/memoize/badge.svg)](https://docs.rs/memoize) 4 | [![Crates.rs](https://img.shields.io/crates/v/memoize.svg)](https://crates.io/crates/memoize) 5 | [![CI](https://github.com/dermesser/rex/workflows/CI/badge.svg)](https://github.com/dermesser/memoize/actions?query=workflow%3ACI) 6 | 7 | A `#[memoize]` attribute for somewhat simple Rust functions: That is, functions 8 | with one or more `Clone`-able arguments, and a `Clone`-able return type. That's it. 9 | 10 | **NEWS**: The crate has been updated so that you don't need to separately import `lru`, 11 | `lazy_static`, and other dependencies. Now everything should work automatically. Remember to 12 | enable the `full` feature to use LRU caching and other additional features. 13 | 14 | Read the documentation (`cargo doc --open`) for the sparse details, or take a 15 | look at the `examples/`, if you want to know more: 16 | 17 | ```rust 18 | // From examples/test2.rs 19 | 20 | use memoize::memoize; 21 | 22 | #[memoize] 23 | fn hello(arg: String, arg2: usize) -> bool { 24 | arg.len()%2 == arg2 25 | } 26 | 27 | fn main() { 28 | // `hello` is only called once here. 29 | assert!(! hello("World".to_string(), 0)); 30 | assert!(! hello("World".to_string(), 0)); 31 | // Sometimes one might need the original function. 32 | assert!(! memoized_original_hello("World".to_string(), 0)); 33 | } 34 | ``` 35 | 36 | This is expanded into (with a few simplifications): 37 | 38 | ```rust 39 | std::thread_local! { 40 | static MEMOIZED_MAPPING_HELLO : RefCell> = RefCell::new(HashMap::new()); 41 | } 42 | 43 | pub fn memoized_original_hello(arg: String, arg2: usize) -> bool { 44 | arg.len() % 2 == arg2 45 | } 46 | 47 | #[allow(unused_variables)] 48 | fn hello(arg: String, arg2: usize) -> bool { 49 | let ATTR_MEMOIZE_RETURN__ = MEMOIZED_MAPPING_HELLO.with(|ATTR_MEMOIZE_HM__| { 50 | let mut ATTR_MEMOIZE_HM__ = ATTR_MEMOIZE_HM__.borrow_mut(); 51 | ATTR_MEMOIZE_HM__.get(&(arg.clone(), arg2.clone())).cloned() 52 | }); 53 | if let Some(ATTR_MEMOIZE_RETURN__) = ATTR_MEMOIZE_RETURN__ { 54 | return ATTR_MEMOIZE_RETURN__; 55 | } 56 | 57 | let ATTR_MEMOIZE_RETURN__ = memoized_original_hello(arg.clone(), arg2.clone()); 58 | 59 | MEMOIZED_MAPPING_HELLO.with(|ATTR_MEMOIZE_HM__| { 60 | let mut ATTR_MEMOIZE_HM__ = ATTR_MEMOIZE_HM__.borrow_mut(); 61 | ATTR_MEMOIZE_HM__.insert((arg, arg2), ATTR_MEMOIZE_RETURN__.clone()); 62 | }); 63 | 64 | r 65 | } 66 | 67 | ``` 68 | 69 | ## Further Functionality 70 | As can be seen in the above example, each thread has its own cache by default. If you would prefer 71 | that every thread share the same cache, you can specify the `SharedCache` option like below to wrap 72 | the cache in a `std::sync::Mutex`. For example: 73 | ```rust 74 | #[memoize(SharedCache)] 75 | fn hello(key: String) -> ComplexStruct { 76 | // ... 77 | } 78 | ``` 79 | 80 | You can choose to use an [LRU cache](https://crates.io/crates/lru). In fact, if 81 | you know that a memoized function has an unbounded number of different inputs, 82 | you should do this! In that case, use the attribute like this: 83 | 84 | ```rust 85 | // From examples/test1.rs 86 | // Compile with --features=full 87 | use memoize::memoize; 88 | 89 | #[derive(Debug, Clone)] 90 | struct ComplexStruct { 91 | // ... 92 | } 93 | 94 | #[memoize(Capacity: 123)] 95 | fn hello(key: String) -> ComplexStruct { 96 | // ... 97 | } 98 | ``` 99 | 100 | Adding more caches and configuration options is relatively simple, and a matter 101 | of parsing attribute parameters. Currently, compiling will fail if you use a 102 | parameter such as `Capacity` without the feature `full` being enabled. 103 | 104 | Another parameter is TimeToLive, specifying how long a cached value is allowed 105 | to live: 106 | 107 | ```rust 108 | #[memoize(Capacity: 123, TimeToLive: Duration::from_secs(2))] 109 | ``` 110 | 111 | `chrono::Duration` is also possible, but would have to first be converted to 112 | `std::time::Duration` 113 | 114 | ```rust 115 | #[memoize(TimeToLive: chrono::Duration::hours(3).to_std().unwrap())] 116 | ``` 117 | 118 | The cached value will never be older than duration provided and instead 119 | recalculated on the next request. 120 | 121 | You can also specifiy a **custom hasher**, like [AHash](https://github.com/tkaitchuck/aHash) using `CustomHasher`. 122 | 123 | ```rust 124 | #[memoize(CustomHasher: ahash::HashMap)] 125 | ``` 126 | 127 | As some hashers initializing functions other than `new()`, you can specifiy a `HasherInit` function call: 128 | 129 | ```rust 130 | #[memoize(CustomHasher: FxHashMap, HasherInit: FxHashMap::default())] 131 | ``` 132 | 133 | Sometimes, you can't or don't want to store data as part of the cache. In those cases, you can use 134 | the `Ignore` parameter in the `#[memoize]` macro to ignore an argument. Any `Ignore`d arguments no 135 | longer need to be `Clone`-able, since they are not stored as part of the argument set, and changing 136 | an `Ignore`d argument will not trigger calling the function again. You can `Ignore` multiple 137 | arugments by specifying the `Ignore` parameter multiple times. 138 | 139 | ```rust 140 | // `Ignore: count_calls` lets our function take a `&mut u32` argument, which is normally not 141 | // possible because it is not `Clone`-able. 142 | #[memoize(Ignore: count_calls)] 143 | fn add(a: u32, b: u32, count_calls: &mut u32) -> u32 { 144 | // Keep track of the number of times the underlying function is called. 145 | *count_calls += 1; 146 | a + b 147 | } 148 | ``` 149 | 150 | ### Flushing 151 | 152 | If you memoize a function `f`, there will be a function called 153 | `memoized_flush_f()` that allows you to clear the memoization cache. 154 | 155 | ## Contributions 156 | 157 | ...are always welcome! This being my first procedural-macros crate, I am 158 | grateful for improvements of functionality and style. Please send a pull 159 | request, and don't be discouraged if it takes a while for me to review it; I'm 160 | sometimes a bit slow to catch up here :) -- Lewin 161 | 162 | -------------------------------------------------------------------------------- /examples/ahash.rs: -------------------------------------------------------------------------------- 1 | use ahash::{HashMap, HashMapExt}; 2 | use memoize::memoize; 3 | 4 | #[cfg(feature = "full")] 5 | #[memoize(CustomHasher: HashMap)] 6 | fn hello() -> bool { 7 | println!("hello!"); 8 | true 9 | } 10 | 11 | #[cfg(feature = "full")] 12 | fn main() { 13 | // `hello` is only called once here. 14 | assert!(hello()); 15 | assert!(hello()); 16 | memoized_flush_hello(); 17 | // and again here. 18 | assert!(hello()); 19 | } 20 | 21 | #[cfg(not(feature = "full"))] 22 | fn main() { 23 | println!("Use the \"full\" feature to execute this example"); 24 | } 25 | -------------------------------------------------------------------------------- /examples/custom_hasher.rs: -------------------------------------------------------------------------------- 1 | //! Reproduces (verifies) issue #17: Panics when used on fn without args. 2 | 3 | use memoize::memoize; 4 | 5 | #[cfg(feature = "full")] 6 | #[memoize(CustomHasher: std::collections::HashMap)] 7 | fn hello() -> bool { 8 | println!("hello!"); 9 | true 10 | } 11 | 12 | // ! This will panic because CustomHasher and Capacity are being used. 13 | // #[cfg(feature = "full")] 14 | // #[memoize(CustomHasher: std::collections::HashMap, Capacity: 3usize)] 15 | // fn will_panic(a: u32, b: u32) -> u32 { 16 | // a + b 17 | // } 18 | 19 | #[cfg(feature = "full")] 20 | fn main() { 21 | // `hello` is only called once here. 22 | assert!(hello()); 23 | assert!(hello()); 24 | memoized_flush_hello(); 25 | // and again here. 26 | assert!(hello()); 27 | } 28 | 29 | #[cfg(not(feature = "full"))] 30 | fn main() { 31 | println!("Use the \"full\" feature to execute this example"); 32 | } 33 | -------------------------------------------------------------------------------- /examples/empty.rs: -------------------------------------------------------------------------------- 1 | //! Reproduces (verifies) issue #17: Panics when used on fn without args. 2 | 3 | use memoize::memoize; 4 | 5 | #[memoize] 6 | fn hello() -> bool { 7 | println!("hello!"); 8 | true 9 | } 10 | 11 | fn main() { 12 | // `hello` is only called once here. 13 | assert!(hello()); 14 | assert_eq!(memoized_size_hello(), 1); 15 | assert!(hello()); 16 | assert_eq!(memoized_size_hello(), 1); 17 | memoized_flush_hello(); 18 | assert_eq!(memoized_size_hello(), 0); 19 | // and again here. 20 | assert!(hello()); 21 | } 22 | -------------------------------------------------------------------------------- /examples/full_featured.rs: -------------------------------------------------------------------------------- 1 | use memoize::memoize; 2 | use std::thread; 3 | use std::time::{Duration, Instant}; 4 | 5 | #[allow(dead_code)] 6 | #[derive(Debug, Clone)] 7 | struct ComplexStruct { 8 | s: String, 9 | b: bool, 10 | i: Instant, 11 | } 12 | 13 | #[cfg(feature = "full")] 14 | #[memoize(TimeToLive: Duration::from_secs(2), Capacity: 123)] 15 | fn hello(key: String) -> ComplexStruct { 16 | println!("hello: {}", key); 17 | ComplexStruct { 18 | s: key, 19 | b: false, 20 | i: Instant::now(), 21 | } 22 | } 23 | 24 | fn main() { 25 | #[cfg(feature = "full")] 26 | { 27 | println!("result: {:?}", hello("ABC".to_string())); 28 | println!("result: {:?}", hello("DEF".to_string())); 29 | println!("result: {:?}", hello("ABC".to_string())); // Same as first 30 | thread::sleep(core::time::Duration::from_millis(2100)); 31 | println!("result: {:?}", hello("ABC".to_string())); 32 | println!("result: {:?}", hello("DEF".to_string())); 33 | println!("result: {:?}", hello("ABC".to_string())); // Same as first 34 | memoized_flush_hello(); 35 | println!("result: {:?}", hello("EFG".to_string())); 36 | println!("result: {:?}", hello("ABC".to_string())); // Refreshed 37 | println!("result: {:?}", hello("EFG".to_string())); // Same as first 38 | println!("result: {:?}", hello("ABC".to_string())); // Same as refreshed 39 | println!("result: {:?}", memoized_original_hello("ABC".to_string())); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/fxhash.rs: -------------------------------------------------------------------------------- 1 | use memoize::memoize; 2 | use rustc_hash::FxHashMap; 3 | 4 | #[cfg(feature = "full")] 5 | #[memoize(CustomHasher: FxHashMap, HasherInit: FxHashMap::default())] 6 | fn hello() -> bool { 7 | println!("hello!"); 8 | true 9 | } 10 | 11 | #[cfg(feature = "full")] 12 | fn main() { 13 | // `hello` is only called once here. 14 | assert!(hello()); 15 | assert!(hello()); 16 | memoized_flush_hello(); 17 | // and again here. 18 | assert!(hello()); 19 | } 20 | 21 | #[cfg(not(feature = "full"))] 22 | fn main() { 23 | println!("Use the \"full\" feature to execute this example"); 24 | } 25 | -------------------------------------------------------------------------------- /examples/ignore.rs: -------------------------------------------------------------------------------- 1 | use memoize::memoize; 2 | 3 | /// Wrapper struct for a [`u32`]. 4 | /// 5 | /// Note that A deliberately does not implement [`Clone`] or [`Hash`], to demonstrate that it can be 6 | /// passed through. 7 | struct C { 8 | c: u32 9 | } 10 | 11 | #[memoize(Ignore: a, Ignore: c)] 12 | fn add(a: u32, b: u32, c: C, d: u32) -> u32 { 13 | a + b + c.c + d 14 | } 15 | 16 | #[memoize(Ignore: call_count, SharedCache)] 17 | fn add2(a: u32, b: u32, call_count: &mut u32) -> u32 { 18 | *call_count += 1; 19 | a + b 20 | } 21 | 22 | fn main() { 23 | // Note that the third argument is not `Clone` but can still be passed through. 24 | assert_eq!(add(1, 2, C {c: 3}, 4), 10); 25 | 26 | assert_eq!(add(3, 2, C {c: 4}, 4), 10); 27 | memoized_flush_add(); 28 | 29 | // Once cleared, all arguments is again used. 30 | assert_eq!(add(3, 2, C {c: 4}, 4), 13); 31 | 32 | let mut count_unique_calls = 0; 33 | assert_eq!(add2(1, 2, &mut count_unique_calls), 3); 34 | assert_eq!(count_unique_calls, 1); 35 | 36 | // Calling `add2` again won't increment `count_unique_calls` 37 | // because it's ignored as a parameter, and the other arguments 38 | // are the same. 39 | add2(1, 2, &mut count_unique_calls); 40 | assert_eq!(count_unique_calls, 1); 41 | } 42 | -------------------------------------------------------------------------------- /examples/issue30.rs: -------------------------------------------------------------------------------- 1 | 2 | use memoize::memoize; 3 | 4 | #[derive(Clone)] 5 | struct Struct {} 6 | #[derive(Clone)] 7 | struct Error {} 8 | 9 | #[cfg(feature = "full")] 10 | #[memoize(SharedCache, Capacity: 1024)] 11 | fn my_function(arg: &'static str) -> Result { 12 | println!("{}", arg); 13 | Ok(Struct{}) 14 | } 15 | 16 | #[cfg(feature = "full")] 17 | fn main() { 18 | let s = "Hello World"; 19 | my_function(s).ok(); 20 | } 21 | 22 | #[cfg(not(feature = "full"))] 23 | fn main() {} 24 | -------------------------------------------------------------------------------- /examples/issue32.rs: -------------------------------------------------------------------------------- 1 | use memoize::memoize; 2 | 3 | #[memoize] 4 | fn expensive(mut foo: i32) -> i32 { 5 | foo += 1; 6 | foo 7 | } 8 | 9 | fn main() { 10 | expensive(7); 11 | } 12 | -------------------------------------------------------------------------------- /examples/patterns.rs: -------------------------------------------------------------------------------- 1 | use memoize::memoize; 2 | 3 | // Patterns in memoized function arguments must be bound by name. 4 | #[memoize] 5 | fn manhattan_distance(_p1 @ (x1, y1): (i32, i32), _p2 @ (x2, y2): (i32, i32)) -> i32 { 6 | (x1 - x2).abs() + (y1 - y2).abs() 7 | } 8 | 9 | #[derive(Clone, PartialEq, Eq, Hash)] 10 | enum OnlyOne { 11 | Value(i32), 12 | } 13 | 14 | #[memoize] 15 | fn get_value(_enum @ OnlyOne::Value(value): OnlyOne) -> i32 { 16 | value 17 | } 18 | 19 | fn main() { 20 | // `manhattan_distance` is only called once here. 21 | assert_eq!(manhattan_distance((1, 1), (1, 3)), 2); 22 | 23 | // Same with `get_value`. 24 | assert_eq!(get_value(OnlyOne::Value(0)), 0); 25 | } 26 | -------------------------------------------------------------------------------- /examples/recursive.rs: -------------------------------------------------------------------------------- 1 | use memoize::memoize; 2 | 3 | #[memoize] 4 | fn fib(n: u64) -> u64 { 5 | if n < 2 { 6 | 1 7 | } else { 8 | fib(n - 1) + fib(n - 2) 9 | } 10 | } 11 | 12 | #[memoize] 13 | fn fac(n: u64) -> u64 { 14 | if n < 2 { 15 | 1 16 | } else { 17 | n * fac(n - 1) 18 | } 19 | } 20 | 21 | fn main() { 22 | let fibs = (0..21).map(fib).collect::>(); 23 | println!("fib([0,...,20]) = {:?}", fibs); 24 | 25 | let facs = (0..21).map(fac).collect::>(); 26 | println!("fac([0,...,20]) = {:?}", facs); 27 | } 28 | -------------------------------------------------------------------------------- /examples/shared.rs: -------------------------------------------------------------------------------- 1 | use memoize::memoize; 2 | 3 | #[memoize(SharedCache)] 4 | fn hello(arg: String, arg2: usize) -> bool { 5 | println!("{} => {}", arg, arg2); 6 | arg.len() % 2 == arg2 7 | } 8 | 9 | fn main() { 10 | // `hello` is only called once here. 11 | assert!(!hello("World".to_string(), 0)); 12 | assert!(!hello("World".to_string(), 0)); 13 | // Sometimes one might need the original function. 14 | assert!(!memoized_original_hello("World".to_string(), 0)); 15 | assert_eq!(memoized_size_hello(), 1); 16 | memoized_flush_hello(); 17 | assert_eq!(memoized_size_hello(), 0); 18 | } 19 | -------------------------------------------------------------------------------- /examples/trivial.rs: -------------------------------------------------------------------------------- 1 | use memoize::memoize; 2 | 3 | #[memoize] 4 | fn hello(arg: String, arg2: usize) -> bool { 5 | println!("{} => {}", arg, arg2); 6 | arg.len() % 2 == arg2 7 | } 8 | 9 | fn main() { 10 | // `hello` is only called once here. 11 | assert!(!hello("World".to_string(), 0)); 12 | assert!(!hello("World".to_string(), 0)); 13 | // Sometimes one might need the original function. 14 | assert!(!memoized_original_hello("World".to_string(), 0)); 15 | memoized_flush_hello(); 16 | } 17 | -------------------------------------------------------------------------------- /inner/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "memoize-inner" 3 | version = "0.5.1" 4 | description = "Helper crate for memoize." 5 | authors = ["Lewin Bormann "] 6 | homepage = "https://github.com/dermesser/memoize" 7 | repository = "https://github.com/dermesser/memoize" 8 | documentation = "https://docs.rs/memoize-inner" 9 | edition = "2018" 10 | license = "MIT" 11 | 12 | [lib] 13 | proc-macro = true 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | lazy_static = "1.4" 18 | proc-macro2 = "1.0" 19 | quote = "1.0" 20 | syn = { version = "1.0", features = ["full"] } 21 | 22 | [features] 23 | default = [] 24 | full = [] 25 | -------------------------------------------------------------------------------- /inner/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![crate_type = "proc-macro"] 2 | #![allow(unused_imports)] // Spurious complaints about a required trait import. 3 | use syn::{self, parse, parse_macro_input, spanned::Spanned, Expr, ExprCall, ItemFn, Path}; 4 | 5 | use proc_macro::TokenStream; 6 | use quote::{self, ToTokens}; 7 | 8 | mod kw { 9 | syn::custom_keyword!(Capacity); 10 | syn::custom_keyword!(TimeToLive); 11 | syn::custom_keyword!(SharedCache); 12 | syn::custom_keyword!(CustomHasher); 13 | syn::custom_keyword!(HasherInit); 14 | syn::custom_keyword!(Ignore); 15 | syn::custom_punctuation!(Colon, :); 16 | } 17 | 18 | #[derive(Default, Clone)] 19 | struct CacheOptions { 20 | lru_max_entries: Option, 21 | time_to_live: Option, 22 | shared_cache: bool, 23 | custom_hasher: Option, 24 | custom_hasher_initializer: Option, 25 | ignore: Vec, 26 | } 27 | 28 | #[derive(Clone)] 29 | enum CacheOption { 30 | LRUMaxEntries(usize), 31 | TimeToLive(Expr), 32 | SharedCache, 33 | CustomHasher(Path), 34 | HasherInit(ExprCall), 35 | Ignore(syn::Ident), 36 | } 37 | 38 | // To extend option parsing, add functionality here. 39 | #[allow(unreachable_code)] 40 | impl parse::Parse for CacheOption { 41 | fn parse(input: parse::ParseStream) -> syn::Result { 42 | let la = input.lookahead1(); 43 | if la.peek(kw::Capacity) { 44 | #[cfg(not(feature = "full"))] 45 | return Err(syn::Error::new(input.span(), 46 | "memoize error: Capacity specified, but the feature 'full' is not enabled! To fix this, compile with `--features=full`.", 47 | )); 48 | 49 | input.parse::().unwrap(); 50 | input.parse::().unwrap(); 51 | let cap: syn::LitInt = input.parse().unwrap(); 52 | 53 | return Ok(CacheOption::LRUMaxEntries(cap.base10_parse()?)); 54 | } 55 | if la.peek(kw::TimeToLive) { 56 | #[cfg(not(feature = "full"))] 57 | return Err(syn::Error::new(input.span(), 58 | "memoize error: TimeToLive specified, but the feature 'full' is not enabled! To fix this, compile with `--features=full`.", 59 | )); 60 | 61 | input.parse::().unwrap(); 62 | input.parse::().unwrap(); 63 | let cap: syn::Expr = input.parse().unwrap(); 64 | 65 | return Ok(CacheOption::TimeToLive(cap)); 66 | } 67 | if la.peek(kw::SharedCache) { 68 | input.parse::().unwrap(); 69 | return Ok(CacheOption::SharedCache); 70 | } 71 | if la.peek(kw::CustomHasher) { 72 | input.parse::().unwrap(); 73 | input.parse::().unwrap(); 74 | let cap: syn::Path = input.parse().unwrap(); 75 | return Ok(CacheOption::CustomHasher(cap)); 76 | } 77 | if la.peek(kw::HasherInit) { 78 | input.parse::().unwrap(); 79 | input.parse::().unwrap(); 80 | let cap: syn::ExprCall = input.parse().unwrap(); 81 | return Ok(CacheOption::HasherInit(cap)); 82 | } 83 | if la.peek(kw::Ignore) { 84 | input.parse::().unwrap(); 85 | input.parse::().unwrap(); 86 | let ignore_ident = input.parse::().unwrap(); 87 | return Ok(CacheOption::Ignore(ignore_ident)); 88 | } 89 | Err(la.error()) 90 | } 91 | } 92 | 93 | impl parse::Parse for CacheOptions { 94 | fn parse(input: parse::ParseStream) -> syn::Result { 95 | let f: syn::punctuated::Punctuated = 96 | input.parse_terminated(CacheOption::parse)?; 97 | let mut opts = Self::default(); 98 | 99 | for opt in f { 100 | match opt { 101 | CacheOption::LRUMaxEntries(cap) => opts.lru_max_entries = Some(cap), 102 | CacheOption::TimeToLive(sec) => opts.time_to_live = Some(sec), 103 | CacheOption::CustomHasher(hasher) => opts.custom_hasher = Some(hasher), 104 | CacheOption::HasherInit(init) => opts.custom_hasher_initializer = Some(init), 105 | CacheOption::SharedCache => opts.shared_cache = true, 106 | CacheOption::Ignore(ident) => opts.ignore.push(ident), 107 | } 108 | } 109 | Ok(opts) 110 | } 111 | } 112 | 113 | // This implementation of the storage backend does not depend on any more crates. 114 | #[cfg(not(feature = "full"))] 115 | mod store { 116 | use crate::CacheOptions; 117 | use proc_macro::TokenStream; 118 | 119 | /// Returns tokenstreams (for quoting) of the store type and an expression to initialize it. 120 | pub(crate) fn construct_cache( 121 | _options: &CacheOptions, 122 | key_type: proc_macro2::TokenStream, 123 | value_type: proc_macro2::TokenStream, 124 | ) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { 125 | // This is the unbounded default. 126 | if let Some(hasher) = &_options.custom_hasher { 127 | return ( 128 | quote::quote! { #hasher<#key_type, #value_type> }, 129 | quote::quote! { #hasher::new() }, 130 | ); 131 | } else { 132 | ( 133 | quote::quote! { std::collections::HashMap<#key_type, #value_type> }, 134 | quote::quote! { std::collections::HashMap::new() }, 135 | ) 136 | } 137 | } 138 | 139 | /// Returns names of methods as TokenStreams to insert and get (respectively) elements from a 140 | /// store. 141 | pub(crate) fn cache_access_methods( 142 | _options: &CacheOptions, 143 | ) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { 144 | (quote::quote! { insert }, quote::quote! { get }) 145 | } 146 | } 147 | 148 | // This implementation of the storage backend also depends on the `lru` crate. 149 | #[cfg(feature = "full")] 150 | mod store { 151 | use crate::CacheOptions; 152 | use proc_macro::TokenStream; 153 | 154 | /// Returns TokenStreams to be used in quote!{} for parametrizing the memoize store variable, 155 | /// and initializing it. 156 | /// 157 | /// First return value: Type of store ("Container"). 158 | /// Second return value: Initializer syntax ("Container::::new()"). 159 | pub(crate) fn construct_cache( 160 | options: &CacheOptions, 161 | key_type: proc_macro2::TokenStream, 162 | value_type: proc_macro2::TokenStream, 163 | ) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { 164 | let value_type = match options.time_to_live { 165 | None => quote::quote! {#value_type}, 166 | Some(_) => quote::quote! {(std::time::Instant, #value_type)}, 167 | }; 168 | // This is the unbounded default. 169 | match options.lru_max_entries { 170 | None => { 171 | if let Some(hasher) = &options.custom_hasher { 172 | if let Some(hasher_init) = &options.custom_hasher_initializer { 173 | return ( 174 | quote::quote! { #hasher<#key_type, #value_type> }, 175 | quote::quote! { #hasher_init }, 176 | ); 177 | } else { 178 | return ( 179 | quote::quote! { #hasher<#key_type, #value_type> }, 180 | quote::quote! { #hasher::new() }, 181 | ); 182 | } 183 | } 184 | ( 185 | quote::quote! { std::collections::HashMap<#key_type, #value_type> }, 186 | quote::quote! { std::collections::HashMap::new() }, 187 | ) 188 | } 189 | Some(cap) => { 190 | if let Some(_) = &options.custom_hasher { 191 | ( 192 | quote::quote! { compile_error!("Cannot use LRU cache and a custom hasher at the same time") }, 193 | quote::quote! { std::collections::HashMap::new() }, 194 | ) 195 | } else { 196 | ( 197 | quote::quote! { ::memoize::lru::LruCache<#key_type, #value_type> }, 198 | quote::quote! { ::memoize::lru::LruCache::new(#cap) }, 199 | ) 200 | } 201 | } 202 | } 203 | } 204 | 205 | /// Returns names of methods as TokenStreams to insert and get (respectively) elements from a 206 | /// store. 207 | pub(crate) fn cache_access_methods( 208 | options: &CacheOptions, 209 | ) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { 210 | // This is the unbounded default. 211 | match options.lru_max_entries { 212 | None => (quote::quote! { insert }, quote::quote! { get }), 213 | Some(_) => (quote::quote! { put }, quote::quote! { get }), 214 | } 215 | } 216 | } 217 | 218 | /** 219 | * memoize is an attribute to create a memoized version of a (simple enough) function. 220 | * 221 | * So far, it works on non-method functions with one or more arguments returning a [`Clone`]-able 222 | * value. Arguments that are cached must be [`Clone`]-able and [`Hash`]-able as well. Several clones 223 | * happen within the storage and recall layer, with the assumption being that `memoize` is used to 224 | * cache such expensive functions that very few `clone()`s do not matter. `memoize` doesn't work on 225 | * methods (functions with `[&/&mut/]self` receiver). 226 | * 227 | * Calls are memoized for the lifetime of a program, using a statically allocated, Mutex-protected 228 | * HashMap. 229 | * 230 | * Memoizing functions is very simple: As long as the above-stated requirements are fulfilled, 231 | * simply use the `#[memoize::memoize]` attribute: 232 | * 233 | * ``` 234 | * use memoize::memoize; 235 | * #[memoize] 236 | * fn hello(arg: String, arg2: usize) -> bool { 237 | * arg.len()%2 == arg2 238 | * } 239 | * 240 | * // `hello` is only called once. 241 | * assert!(! hello("World".to_string(), 0)); 242 | * assert!(! hello("World".to_string(), 0)); 243 | * ``` 244 | * 245 | * If you need to use the un-memoized function, it is always available as `memoized_original_{fn}`, 246 | * in this case: `memoized_original_hello()`. 247 | * 248 | * Parameters can be ignored by the cache using the `Ignore` parameter. `Ignore` can be specified 249 | * multiple times, once per each parameter. `Ignore`d parameters do not need to implement [`Clone`] 250 | * or [`Hash`]. 251 | * 252 | * See the `examples` for concrete applications. 253 | * 254 | * *The following descriptions need the `full` feature enabled.* 255 | * 256 | * The `memoize` attribute can take further parameters in order to use an LRU cache: 257 | * `#[memoize(Capacity: 1234)]`. In that case, instead of a `HashMap` we use an `lru::LruCache` 258 | * with the given capacity. 259 | * `#[memoize(TimeToLive: Duration::from_secs(2))]`. In that case, cached value will be actual 260 | * no longer than duration provided and refreshed with next request. If you prefer chrono::Duration, 261 | * it can be also used: `#[memoize(TimeToLive: chrono::Duration::hours(9).to_std().unwrap()]` 262 | * 263 | * You can also specify a custom hasher: `#[memoize(CustomHasher: ahash::HashMap)]`, as some hashers don't use a `new()` method to initialize them, you can also specifiy a `HasherInit` parameter, like this: `#[memoize(CustomHasher: FxHashMap, HasherInit: FxHashMap::default())]`, so it will initialize your `FxHashMap` with `FxHashMap::default()` insteado of `FxHashMap::new()` 264 | * 265 | * This mechanism can, in principle, be extended (in the source code) to any other cache mechanism. 266 | * 267 | * `memoized_flush_()` allows you to clear the underlying memoization cache of a 268 | * function. This function is generated with the same visibility as the memoized function. 269 | * 270 | */ 271 | #[proc_macro_attribute] 272 | pub fn memoize(attr: TokenStream, item: TokenStream) -> TokenStream { 273 | let func = parse_macro_input!(item as ItemFn); 274 | let sig = &func.sig; 275 | 276 | let fn_name = &sig.ident.to_string(); 277 | let renamed_name = format!("memoized_original_{}", fn_name); 278 | let flush_name = syn::Ident::new(format!("memoized_flush_{}", fn_name).as_str(), sig.span()); 279 | let size_name = syn::Ident::new(format!("memoized_size_{}", fn_name).as_str(), sig.span()); 280 | let map_name = format!("memoized_mapping_{}", fn_name); 281 | 282 | if let Some(syn::FnArg::Receiver(_)) = sig.inputs.first() { 283 | return quote::quote! { compile_error!("Cannot memoize methods!"); }.into(); 284 | } 285 | 286 | // Parse options from macro attributes 287 | let options: CacheOptions = syn::parse(attr.clone()).unwrap(); 288 | 289 | // Extracted from the function signature. 290 | let input_params = match check_signature(sig, &options) { 291 | Ok(p) => p, 292 | Err(e) => return e.to_compile_error().into(), 293 | }; 294 | 295 | // Input types and names that are actually stored in the cache. 296 | let memoized_input_types: Vec> = input_params 297 | .iter() 298 | .filter_map(|p| { 299 | if p.is_memoized { 300 | Some(p.arg_type.clone()) 301 | } else { 302 | None 303 | } 304 | }) 305 | .collect(); 306 | let memoized_input_names: Vec = input_params 307 | .iter() 308 | .filter_map(|p| { 309 | if p.is_memoized { 310 | Some(p.arg_name.clone()) 311 | } else { 312 | None 313 | } 314 | }) 315 | .collect(); 316 | 317 | // For each input, expression to be passe through to the original function. 318 | // Cached arguments are cloned, original arguments are forwarded as-is 319 | let fn_forwarded_exprs: Vec<_> = input_params 320 | .iter() 321 | .map(|p| { 322 | let ident = p.arg_name.clone(); 323 | if p.is_memoized { 324 | quote::quote! { #ident.clone() } 325 | } else { 326 | quote::quote! { #ident } 327 | } 328 | }) 329 | .collect(); 330 | 331 | let input_tuple_type = quote::quote! { (#(#memoized_input_types),*) }; 332 | let return_type = match &sig.output { 333 | syn::ReturnType::Default => quote::quote! { () }, 334 | syn::ReturnType::Type(_, ty) => ty.to_token_stream(), 335 | }; 336 | 337 | // Construct storage for the memoized keys and return values. 338 | let store_ident = syn::Ident::new(&map_name.to_uppercase(), sig.span()); 339 | let (cache_type, cache_init) = 340 | store::construct_cache(&options, input_tuple_type, return_type.clone()); 341 | let store = if options.shared_cache { 342 | quote::quote! { 343 | ::memoize::lazy_static::lazy_static! { 344 | static ref #store_ident : std::sync::Mutex<#cache_type> = 345 | std::sync::Mutex::new(#cache_init); 346 | } 347 | } 348 | } else { 349 | quote::quote! { 350 | std::thread_local! { 351 | static #store_ident : std::cell::RefCell<#cache_type> = 352 | std::cell::RefCell::new(#cache_init); 353 | } 354 | } 355 | }; 356 | 357 | // Rename original function. 358 | let mut renamed_fn = func.clone(); 359 | renamed_fn.sig.ident = syn::Ident::new(&renamed_name, func.sig.span()); 360 | let memoized_id = &renamed_fn.sig.ident; 361 | 362 | // Construct memoizer function, which calls the original function. 363 | let syntax_names_tuple = quote::quote! { (#(#memoized_input_names),*) }; 364 | let syntax_names_tuple_cloned = quote::quote! { (#(#memoized_input_names.clone()),*) }; 365 | let forwarding_tuple = quote::quote! { (#(#fn_forwarded_exprs),*) }; 366 | let (insert_fn, get_fn) = store::cache_access_methods(&options); 367 | let (read_memo, memoize) = match options.time_to_live { 368 | None => ( 369 | quote::quote!(ATTR_MEMOIZE_HM__.#get_fn(&#syntax_names_tuple_cloned).cloned()), 370 | quote::quote!(ATTR_MEMOIZE_HM__.#insert_fn(#syntax_names_tuple, ATTR_MEMOIZE_RETURN__.clone());), 371 | ), 372 | Some(ttl) => ( 373 | quote::quote! { 374 | ATTR_MEMOIZE_HM__.#get_fn(&#syntax_names_tuple_cloned).and_then(|(last_updated, ATTR_MEMOIZE_RETURN__)| 375 | (last_updated.elapsed() < #ttl).then(|| ATTR_MEMOIZE_RETURN__.clone()) 376 | ) 377 | }, 378 | quote::quote!(ATTR_MEMOIZE_HM__.#insert_fn(#syntax_names_tuple, (std::time::Instant::now(), ATTR_MEMOIZE_RETURN__.clone()));), 379 | ), 380 | }; 381 | 382 | let memoizer = if options.shared_cache { 383 | quote::quote! { 384 | { 385 | let mut ATTR_MEMOIZE_HM__ = #store_ident.lock().unwrap(); 386 | if let Some(ATTR_MEMOIZE_RETURN__) = #read_memo { 387 | return ATTR_MEMOIZE_RETURN__ 388 | } 389 | } 390 | let ATTR_MEMOIZE_RETURN__ = #memoized_id #forwarding_tuple; 391 | 392 | let mut ATTR_MEMOIZE_HM__ = #store_ident.lock().unwrap(); 393 | #memoize 394 | 395 | ATTR_MEMOIZE_RETURN__ 396 | } 397 | } else { 398 | quote::quote! { 399 | let ATTR_MEMOIZE_RETURN__ = #store_ident.with(|ATTR_MEMOIZE_HM__| { 400 | let mut ATTR_MEMOIZE_HM__ = ATTR_MEMOIZE_HM__.borrow_mut(); 401 | #read_memo 402 | }); 403 | if let Some(ATTR_MEMOIZE_RETURN__) = ATTR_MEMOIZE_RETURN__ { 404 | return ATTR_MEMOIZE_RETURN__; 405 | } 406 | 407 | let ATTR_MEMOIZE_RETURN__ = #memoized_id #forwarding_tuple; 408 | 409 | #store_ident.with(|ATTR_MEMOIZE_HM__| { 410 | let mut ATTR_MEMOIZE_HM__ = ATTR_MEMOIZE_HM__.borrow_mut(); 411 | #memoize 412 | }); 413 | 414 | ATTR_MEMOIZE_RETURN__ 415 | } 416 | }; 417 | 418 | let vis = &func.vis; 419 | 420 | let flusher = if options.shared_cache { 421 | quote::quote! { 422 | #vis fn #flush_name() { 423 | #store_ident.lock().unwrap().clear(); 424 | } 425 | } 426 | } else { 427 | quote::quote! { 428 | #vis fn #flush_name() { 429 | #store_ident.with(|ATTR_MEMOIZE_HM__| ATTR_MEMOIZE_HM__.borrow_mut().clear()); 430 | } 431 | } 432 | }; 433 | 434 | let size_func = if options.shared_cache { 435 | quote::quote! { 436 | #vis fn #size_name() -> usize { 437 | #store_ident.lock().unwrap().len() 438 | } 439 | } 440 | } else { 441 | quote::quote! { 442 | #vis fn #size_name() -> usize { 443 | #store_ident.with(|ATTR_MEMOIZE_HM__| ATTR_MEMOIZE_HM__.borrow().len()) 444 | } 445 | } 446 | }; 447 | 448 | quote::quote! { 449 | #renamed_fn 450 | #flusher 451 | #size_func 452 | #store 453 | 454 | #[allow(unused_variables, unused_mut)] 455 | #vis #sig { 456 | #memoizer 457 | } 458 | } 459 | .into() 460 | } 461 | 462 | /// An argument of the memoized function. 463 | struct FnArgument { 464 | /// Type of the argument. 465 | arg_type: Box, 466 | 467 | /// Identifier (name) of the argument. 468 | arg_name: syn::Ident, 469 | 470 | /// Whether or not this specific argument is included in the memoization. 471 | is_memoized: bool, 472 | } 473 | 474 | fn check_signature( 475 | sig: &syn::Signature, 476 | options: &CacheOptions, 477 | ) -> Result, syn::Error> { 478 | if sig.inputs.is_empty() { 479 | return Ok(vec![]); 480 | } 481 | 482 | let mut params = vec![]; 483 | 484 | for a in &sig.inputs { 485 | if let syn::FnArg::Typed(ref arg) = a { 486 | let arg_type = arg.ty.clone(); 487 | 488 | if let syn::Pat::Ident(patident) = &*arg.pat { 489 | let arg_name = patident.ident.clone(); 490 | let is_memoized = !options.ignore.contains(&arg_name); 491 | params.push(FnArgument { 492 | arg_type, 493 | arg_name, 494 | is_memoized, 495 | }); 496 | } else { 497 | return Err(syn::Error::new( 498 | sig.span(), 499 | "Cannot memoize arbitrary patterns!", 500 | )); 501 | } 502 | } 503 | } 504 | Ok(params) 505 | } 506 | 507 | #[cfg(test)] 508 | mod tests {} 509 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use ::lazy_static; 2 | pub use ::memoize_inner::memoize; 3 | 4 | #[cfg(feature = "full")] 5 | pub use ::lru; 6 | --------------------------------------------------------------------------------