function memrec(fn) {
const name = fn.name;
const src = fn.toString();
let hash = ""
for(let i = 0; i < 64;i++) { hash += (Math.random()*32|0).toString(32); }
let fn_name = "fn_" + hash;
let i = src.indexOf("(");
let j = src.indexOf(")", i);
if (i+1 === j) {
const v = fn();
return () => v;
}
// return n <= 1 ? 1 : cb(cb, n - 1) + cb(cb, n - 2);
// };
//
// That might seem a bit silly at first, but this allows us to make a memoized
// version of it later.
// Count number of parameters.
// Maybe slightly less fragile in the face of default values with strings with commas
// Does not handle every case, breaks when we start embedding strings in strings with
// single quotes, double quotes and backticks, not to mention escape codes for example.
// Split string by commas, but ignore commas inside quotes
// https://stackoverflow.com/questions/23582276/split-string-by-comma-but-ignore-commas-inside-quotes#comment71514557_23582323
const paramRegex = /,(?=(?:(?:[^'"]*(?:'|")){2})*[^'"]*$)/;
let params = src.substring(i+1, j).split(paramRegex);
// we're going to assume that the first time we see "{" in our source
// after the parameters it will be the start of the function body, and
// that the last "}" ends it.
const src_body = src.substring(src.indexOf("{", j) + 1, src.lastIndexOf("}"))
// find and replace all recursive calls with the new function name, and insert the new function as
// the first parameter.
const fnRegexp = new RegExp(`([;:{\\s])${name}(\\s*\\()`, 'g');
const rec_body = src_body.replace(fnRegexp, `$1${fn_name}(${fn_name}, `);
// create a new function
params.unshift(fn_name);
params.push(rec_body);
const newfn = new Function(...params);
// Now we're ready to create a memoized version. The trick is to create a memoized
// wrapper cachefn that calls newfn, while passing itself instead of newfn as the
// function to be called to newfn.
// What this means is that newfn will call cachefn when doing recursion. If the
// new parameters have a result that is memoized, cachefn will return it. If not,
// it will call newfn with the changed parameters. This is a form of mutual
// recursion that results in the cache lookup being interjected in the function
// call at the right position.
const cache = new ParamCache();
const cachefn = (...args) => {
let v = cache.get(args);
if (!v.length) {
v[0] = newfn(...args);
cache.set(args, v[0]);
}
return v[0];
}
// we don't want to expose all this weirdness of mutual-recursive-self-passing,
// so we create a final wrapper that accepts the original arguments and return it
return (...args) => {
return cachefn(cachefn, ...args);
};
}