1 module memoize;
2 
3 import std.traits;
4 import tuple_modifiers;
5 
6 /**
7 The following code makes cached (memoized) property `f`
8 ```
9 class {
10     @property string _f() { ... }
11     mixin CachedProperty!"f";
12 }
13 ```
14 */
15 mixin template CachedProperty(string name, string baseName = '_' ~ name) {
16     mixin("private typeof(" ~ baseName ~ ") " ~ name ~ "Cache;");
17     mixin("private bool " ~ name ~ "IsCached = false;");
18     mixin("@property typeof(" ~ baseName ~ ") " ~ name ~ "() {\n" ~
19           "if (" ~ name ~ "IsCached" ~ ") return " ~ name ~ "Cache;\n" ~
20           name ~ "IsCached = true;\n" ~
21           "return " ~ name ~ "Cache = " ~ baseName ~ ";\n" ~
22           '}');
23 }
24 
25 unittest {
26     struct S {
27         @property float _x() { return 1.5; }
28         mixin CachedProperty!"x";
29     }
30     class C {
31         @property float _x() { return 1.5; }
32         mixin CachedProperty!"x";
33     }
34     S s;
35     assert(s.x == 1.5);
36     assert(s.x == 1.5);
37     C c = new C();
38     assert(c.x == 1.5);
39 }
40 
41 // Ugh, repeated code
42 private template _memoize(alias fun, string attr)
43 {
44     // alias Args = addTupleModifiers!("const", Parameters!fun); // Bugzilla 13580
45 
46     ReturnType!fun _memoize(Parameters!fun args)
47     {
48         alias Args = addTupleModifiers!("const", Parameters!fun);
49         import std.typecons : Tuple;
50 
51         mixin(attr ~ " static Unqual!(ReturnType!fun)[Tuple!Args] memo;");
52         auto t = Tuple!Args(args);
53         if (auto p = t in memo)
54             return *p;
55         return memo[t] = fun(args);
56     }
57 }
58 
59 private template _referenceMemoize(alias fun, string attr)
60 {
61     // alias Args = addTupleModifiers!("const", Parameters!fun); // Bugzilla 13580
62 
63     ref Unqual!(ReturnType!fun) _referenceMemoize(Parameters!fun args)
64     {
65         alias Args = addTupleModifiers!("const", Parameters!fun);
66         import std.typecons : Tuple;
67 
68         static ReturnType!fun*[Tuple!Args] memo;
69         auto t = Tuple!Args(args);
70         if (auto p = t in memo) return **p;
71         memo[t] = &fun(args);
72         return *memo[t];
73     }
74 }
75 
76 // TODO: It does not compile when modifier != ""
77 // (also compiling with different modifiers would use more program memory)
78 private template _memoize(alias fun, uint maxSize, string modifier)
79 {
80     // alias Args = addTupleModifiers!("const", Parameters!fun); // Bugzilla 13580
81     // Ugh, repeated code
82     ReturnType!fun _memoize(Parameters!fun args)
83     {
84         import std.traits : hasIndirections;
85         import std.typecons : tuple;
86         static struct Value { Parameters!fun args; ReturnType!fun res; }
87         mixin(modifier ~ " static Unqual!(Value)[] memo;");
88         mixin(modifier ~ " static size_t[] initialized;");
89 
90         if (!memo.length)
91         {
92             import core.memory : GC;
93 
94             // Ensure no allocation overflows
95             static assert(maxSize < size_t.max / Value.sizeof);
96             static assert(maxSize < size_t.max - (8 * size_t.sizeof - 1));
97 
98             enum attr = GC.BlkAttr.NO_INTERIOR | (hasIndirections!Value ? 0 : GC.BlkAttr.NO_SCAN);
99             mixin("alias VType = " ~ modifier ~ " Value;");
100             memo = (cast(VType*) GC.malloc(Value.sizeof * maxSize, attr))[0 .. maxSize];
101             enum nwords = (maxSize + 8 * size_t.sizeof - 1) / (8 * size_t.sizeof);
102             mixin("alias mysize_t = " ~ modifier ~ " size_t;");
103             initialized = (cast(mysize_t*) GC.calloc(nwords * size_t.sizeof, attr | GC.BlkAttr.NO_SCAN))[0 .. nwords];
104         }
105 
106         import core.bitop : bt, bts;
107         import std.conv : emplace;
108 
109         size_t hash;
110         foreach (ref arg; args)
111             hash = hashOf(arg, hash);
112         // cuckoo hashing
113         immutable idx1 = hash % maxSize;
114         if (!bt(cast(size_t*) initialized.ptr, idx1))
115         {
116             emplace(cast(Unqual!(Value*)) &memo[idx1], args, fun(args));
117             bts(cast(size_t*) initialized.ptr, idx1); // only set to initialized after setting args and value (bugzilla 14025)
118             return memo[idx1].res;
119         }
120         else if (memo[idx1].args == args)
121             return memo[idx1].res;
122         // FNV prime
123         immutable idx2 = (hash * 16_777_619) % maxSize;
124         if (!bt(cast(size_t*) initialized.ptr, idx2))
125         {
126             emplace(&memo[idx2], memo[idx1]);
127             bts(cast(size_t*) initialized.ptr, idx2); // only set to initialized after setting args and value (bugzilla 14025)
128         }
129         else if (memo[idx2].args == args)
130             return memo[idx2].res;
131         else if (idx1 != idx2)
132             memo[idx2] = memo[idx1];
133 
134         memo[idx1] = Value(args, fun(args));
135         return memo[idx1].res;
136     }
137 }
138 
139 private template _referenceMemoize(alias fun, uint maxSize, string modifier)
140 {
141     ref Unqual!(ReturnType!fun) _referenceMemoize(Parameters!fun args)
142     {
143         import std.traits : hasIndirections;
144         import std.typecons : tuple;
145         static struct Value { Parameters!fun args; ReturnType!fun *res; }
146         mixin(modifier ~ " static Unqual!(Value)*[] memo;");
147         mixin(modifier ~ " static size_t[] initialized;");
148 
149         if (!memo.length)
150         {
151             import core.memory : GC;
152 
153             // Ensure no allocation overflows
154             static assert(maxSize < size_t.max / Value.sizeof);
155             static assert(maxSize < size_t.max - (8 * size_t.sizeof - 1));
156 
157             enum attr = GC.BlkAttr.NO_INTERIOR | (hasIndirections!Value ? 0 : GC.BlkAttr.NO_SCAN);
158             mixin("alias VType = " ~ modifier ~ " Value;");
159             memo = (cast(VType*) GC.malloc(Value.sizeof * maxSize, attr))[0 .. maxSize];
160             enum nwords = (maxSize + 8 * size_t.sizeof - 1) / (8 * size_t.sizeof);
161             mixin("alias mysize_t = " ~ modifier ~ " size_t;");
162             initialized = (cast(mysize_t*) GC.calloc(nwords * size_t.sizeof, attr | GC.BlkAttr.NO_SCAN))[0 .. nwords];
163         }
164 
165         import core.bitop : bt, bts;
166         import std.conv : emplace;
167 
168         size_t hash;
169         foreach (ref arg; args)
170             hash = hashOf(arg, hash);
171         // cuckoo hashing
172         immutable idx1 = hash % maxSize;
173         if (!bt(cast(size_t*) initialized.ptr, idx1))
174         {
175             emplace(&memo[idx1], args, &fun(args));
176             bts(cast(size_t*) initialized.ptr, idx1); // only set to initialized after setting args and value (bugzilla 14025)
177             return *memo[idx1].res;
178         }
179         else if (memo[idx1].args == args)
180             return *memo[idx1].res;
181         // FNV prime
182         immutable idx2 = (hash * 16_777_619) % maxSize;
183         if (!bt(cast(size_t*) initialized.ptr, idx2))
184         {
185             emplace(&memo[idx2], memo[idx1]);
186             bts(cast(size_t*) initialized.ptr, idx2); // only set to initialized after setting args and value (bugzilla 14025)
187         }
188         else if (memo[idx2].args == args)
189             return *memo[idx2].res;
190         else if (idx1 != idx2)
191             memo[idx2] = memo[idx1];
192 
193         memo[idx1] = Value(args, &fun(args));
194         return *memo[idx1].res;
195     }
196 }
197 
198 // See https://issues.dlang.org/show_bug.cgi?id=4533 why aliases are not used.
199 
200 /**
201 The same as in Phobos `std.functional`.
202 */
203 //alias memoize(alias fun) = _memoize!(fun, "");
204 template memoize(alias fun)
205 {
206     ReturnType!fun memoize(Parameters!fun args)
207     {
208         return _memoize!(fun, "")(args);
209     }
210 }
211 
212 template referenceMemoize(alias fun)
213 {
214     ref Unqual!(ReturnType!fun) referenceMemoize(Parameters!fun args)
215     {
216         return _referenceMemoize!(fun, "")(args);
217     }
218 }
219 
220 /// ditto
221 //alias memoize(alias fun, uint maxSize) = _memoize!(fun, maxSize, "");
222 template memoize(alias fun, uint maxSize)
223 {
224     ReturnType!fun memoize(Parameters!fun args)
225     {
226         return _memoize!(fun, maxSize, "")(args);
227     }
228 }
229 
230 template referenceMemoize(alias fun, uint maxSize)
231 {
232     ref Unqual!(ReturnType!fun) referenceMemoize(Parameters!fun args)
233     {
234         return _referenceMemoize!(fun, maxSize, "")(args);
235     }
236 }
237 
238 /// must be locked explicitly!
239 //alias noLockMemoize(alias fun) = _memoize!(fun, "shared");
240 template noLockMemoize(alias fun)
241 {
242     ReturnType!fun noLockMemoize(Parameters!fun args)
243     {
244         return _memoize!(fun, "shared")(args);
245     }
246 }
247 
248 template referenceNoLockMemoize(alias fun)
249 {
250     ref Unqual!(ReturnType!fun) referenceNoLockMemoize(Parameters!fun args)
251     {
252         return _referenceMemoize!(fun, "shared")(args);
253     }
254 }
255 
256 /// must be locked explicitly!
257 //alias noLockMemoize(alias fun, uint maxSize) = _memoize!(fun, maxSize, "shared");
258 template noLockMemoize(alias fun, uint maxSize)
259 {
260     ReturnType!fun noLockMemoize(Parameters!fun args)
261     {
262         //return _memoize!(fun, maxSize, "shared")(args); // does not compile
263         return _memoize!(fun, maxSize, "")(args);
264     }
265 }
266 
267 template referenceNoLockMemoize(alias fun, uint maxSize)
268 {
269     ref Unqual!(ReturnType!fun) referenceNoLockMemoize(Parameters!fun args)
270     {
271         return _referenceMemoize!(fun, maxSize, "shared")(args);
272     }
273 }
274 
275 /**
276 Synchronized version of `memoize` using global (interthread) cache.
277 */
278 template synchronizedMemoize(alias fun) {
279     private alias impl = memoize!fun;
280     ReturnType!fun synchronizedMemoize(Parameters!fun args) {
281         synchronized {
282             return impl(args);
283         }
284     }
285 }
286 
287 template referenceSynchronizedMemoize(alias fun) {
288     private alias impl = referenceMemoize!fun;
289     ref Unqual!(ReturnType!fun) referenceSynchronizedMemoize(Parameters!fun args) {
290         synchronized {
291             return impl(args);
292         }
293     }
294 }
295 
296 /// ditto
297 template synchronizedMemoize(alias fun, uint maxSize) {
298     private alias impl = memoize!(fun, maxSize);
299     ReturnType!fun synchronizedMemoize(Parameters!fun args) {
300         synchronized {
301             return impl(args);
302         }
303     }
304 }
305 
306 template referenceSynchronizedMemoize(alias fun, uint maxSize) {
307     private alias impl = referenceMemoize!(fun, maxSize);
308     ref Unqual!(ReturnType!fun) referenceSynchronizedMemoize(Parameters!fun args) {
309         synchronized {
310             return impl(args);
311         }
312     }
313 }
314 
315 @safe unittest
316 {
317     ulong fib(ulong n) @safe
318     {
319         return n < 2 ? n : memoize!fib(n - 2) + memoize!fib(n - 1);
320     }
321     assert(fib(10) == 55);
322     ulong fib2(ulong n) @safe
323     {
324         return n < 2 ? n : synchronizedMemoize!fib2(n - 2) + synchronizedMemoize!fib2(n - 1);
325     }
326     assert(fib2(10) == 55);
327     ulong fib3(ulong n) @safe
328     {
329         return n < 2 ? n : noLockMemoize!fib3(n - 2) + noLockMemoize!fib3(n - 1);
330     }
331     assert(fib3(10) == 55);
332 }
333 
334 @safe unittest
335 {
336     ulong fact(ulong n) @safe
337     {
338         return n < 2 ? 1 : n * memoize!fact(n - 1);
339     }
340     assert(fact(10) == 3628800);
341     ulong fact2(ulong n) @safe
342     {
343         return n < 2 ? 1 : n * synchronizedMemoize!fact2(n - 1);
344     }
345     assert(fact2(10) == 3628800);
346     ulong fact3(ulong n) @safe
347     {
348         return n < 2 ? 1 : n * noLockMemoize!fact3(n - 1);
349     }
350     assert(fact3(10) == 3628800);
351 }
352 
353 @safe unittest
354 {
355     ulong factImpl(ulong n) @safe
356     {
357         return n < 2 ? 1 : n * factImpl(n - 1);
358     }
359     alias fact = memoize!factImpl;
360     assert(fact(10) == 3628800);
361     ulong factImpl2(ulong n) @safe
362     {
363         return n < 2 ? 1 : n * factImpl2(n - 1);
364     }
365     alias fact2 = synchronizedMemoize!factImpl;
366     assert(fact2(10) == 3628800);
367     ulong factImpl3(ulong n) @safe
368     {
369         return n < 2 ? 1 : n * factImpl3(n - 1);
370     }
371     alias fact3 = noLockMemoize!factImpl;
372     assert(fact3(10) == 3628800);
373 }
374 
375 @system unittest // not @safe due to memoize
376 {
377     ulong fact(ulong n)
378     {
379         // Memoize no more than 8 values
380         return n < 2 ? 1 : n * memoize!(fact, 8)(n - 1);
381     }
382     assert(fact(8) == 40320);
383     // using more entries than maxSize will overwrite existing entries
384     assert(fact(10) == 3628800);
385     ulong fact2(ulong n)
386     {
387         // Memoize no more than 8 values
388         return n < 2 ? 1 : n * synchronizedMemoize!(fact2, 8)(n - 1);
389     }
390     assert(fact2(8) == 40320);
391     // using more entries than maxSize will overwrite existing entries
392     assert(fact2(10) == 3628800);
393     ulong fact3(ulong n)
394     {
395         // Memoize no more than 8 values
396         return n < 2 ? 1 : n * noLockMemoize!(fact3, 8)(n - 1);
397     }
398     assert(fact3(8) == 40320);
399     // using more entries than maxSize will overwrite existing entries
400     assert(fact3(10) == 3628800);
401 }
402 
403 @system unittest
404 {
405     static ulong x;
406     ref ulong f()
407     {
408         return x;
409     }
410     assert(&referenceMemoize!f() == &x);
411     assert(&referenceSynchronizedMemoize!f() == &x);
412     assert(&referenceNoLockMemoize!f() == &x);
413 }
414 
415 /**
416 Use it to memoize both a struct or class instance for a member function and function arguments like:
417 ```
418 struct S {
419     int f(int a, int b) {
420         return a + b;
421     }
422 }
423 alias f2 = memoizeMember!(S, "f");
424 alias f3 = memoizeMember!(S, "f", 10);
425 S s;
426 assert(f2(s, 2, 3) == 5);
427 ```
428 
429 As you see the member function name ("f" in the example) is passed as a string.
430 This is very unnatural, but I found no other way to do it.
431 */
432 template memoizeMember(S, string name) {
433     alias Member = __traits(getMember, S, name);
434     ReturnType!Member f(S s, Parameters!Member others) {
435         return __traits(getMember, s, name)(others);
436     }
437     alias memoizeMember = memoize!f;
438 }
439 
440 template referenceMemoizeMember(S, string name) {
441     alias Member = __traits(getMember, S, name);
442     ref ReturnType!Member f(S s, Parameters!Member others) {
443         return __traits(getMember, s, name)(others);
444     }
445     alias referenceMemoizeMember = referenceMemoize!f;
446 }
447 
448 /// ditto
449 template memoizeMember(S, string name, uint maxSize) {
450     alias Member = __traits(getMember, S, name);
451     ReturnType!Member f(S s, Parameters!Member others) {
452         return __traits(getMember, s, name)(others);
453     }
454     alias memoizeMember = memoize!(f, maxSize);
455 }
456 
457 template referenceMemoizeMember(S, string name, uint maxSize) {
458     alias Member = __traits(getMember, S, name);
459     ref ReturnType!Member f(S s, Parameters!Member others) {
460         return __traits(getMember, s, name)(others);
461     }
462     alias referenceMemoizeMember = referenceMemoize!(f, maxSize);
463 }
464 
465 /// ditto
466 template noLockMemoizeMember(S, string name) {
467     alias Member = __traits(getMember, S, name);
468     ReturnType!Member f(S s, Parameters!Member others) {
469         return __traits(getMember, s, name)(others);
470     }
471     alias noLockMemoizeMember = noLockMemoize!f;
472 }
473 
474 template referenceNoLockMemoizeMember(S, string name) {
475     alias Member = __traits(getMember, S, name);
476     ref ReturnType!Member f(S s, Parameters!Member others) {
477         return __traits(getMember, s, name)(others);
478     }
479     alias referenceNoLockMemoizeMember = referenceNoLockMemoize!f;
480 }
481 
482 /// ditto
483 template noLockMemoizeMember(S, string name, uint maxSize) {
484     alias Member = __traits(getMember, S, name);
485     ReturnType!Member f(S s, Parameters!Member others) {
486         return __traits(getMember, s, name)(others);
487     }
488     alias noLockMemoizeMember = noLockMemoize!(f, maxSize);
489 }
490 
491 template referenceNoLockMemoizeMember(S, string name, uint maxSize) {
492     alias Member = __traits(getMember, S, name);
493     ref ReturnType!Member f(S s, Parameters!Member others) {
494         return __traits(getMember, s, name)(others);
495     }
496     alias referenceNoLockMemoizeMember = referenceNoLockMemoize!(f, maxSize);
497 }
498 
499 /// ditto
500 template synchronizedMemoizeMember(S, string name) {
501     alias Member = __traits(getMember, S, name);
502     ReturnType!Member f(S s, Parameters!Member others) {
503         return __traits(getMember, s, name)(others);
504     }
505     alias synchronizedMemoizeMember = synchronizedMemoize!f;
506 }
507 
508 template referenceSynchronizedMemoizeMember(S, string name) {
509     alias Member = __traits(getMember, S, name);
510     ref ReturnType!Member f(S s, Parameters!Member others) {
511         return __traits(getMember, s, name)(others);
512     }
513     alias referenceSynchronizedMemoizeMember = referenceSynchronizedMemoize!f;
514 }
515 
516 /// ditto
517 template synchronizedMemoizeMember(S, string name, uint maxSize) {
518     alias Member = __traits(getMember, S, name);
519     ReturnType!Member f(S s, Parameters!Member others) {
520         return __traits(getMember, s, name)(others);
521     }
522     alias synchronizedMemoizeMember = synchronizedMemoize!(f, maxSize);
523 }
524 
525 template referenceSynchronizedMemoizeMember(S, string name, uint maxSize) {
526     alias Member = __traits(getMember, S, name);
527     ref ReturnType!Member f(S s, Parameters!Member others) {
528         return __traits(getMember, s, name)(others);
529     }
530     alias referenceSynchronizedMemoizeMember = referenceSynchronizedMemoize!(f, maxSize);
531 }
532 
533 unittest {
534     struct S2 {
535         int f(int a, int b) {
536             return a + b;
537         }
538     }
539     alias f2 = memoizeMember!(S2, "f");
540     alias f3 = memoizeMember!(S2, "f", 10);
541     alias f4 = synchronizedMemoizeMember!(S2, "f");
542     alias f5 = synchronizedMemoizeMember!(S2, "f", 10);
543     alias f6 = noLockMemoizeMember!(S2, "f");
544     alias f7 = noLockMemoizeMember!(S2, "f", 10);
545     S2 s;
546     assert(f2(s, 2, 3) == 5);
547     assert(f2(s, 2, 3) == 5);
548     assert(f3(s, 2, 3) == 5);
549     assert(f4(s, 2, 3) == 5);
550     assert(f5(s, 2, 3) == 5);
551     assert(f6(s, 2, 3) == 5);
552     assert(f7(s, 2, 3) == 5);
553 }
554 
555 unittest {
556     int x;
557     struct S2 {
558         ref int f() {
559             return x;
560         }
561     }
562     S2 s;
563     assert(&referenceMemoizeMember!(S2, "f")(s) == &x);
564     assert(&referenceSynchronizedMemoizeMember!(S2, "f")(s) == &x);
565     assert(&referenceNoLockMemoizeMember!(S2, "f")(s) == &x);
566 }