* @license MIT */ class Memoization { /** * Stack of cache objects for current function. * * @var SplStack */ private SplStack $cache; /** * Stack of stmts to prepend for current function. * * @var SplStack */ private SplStack $stmts; /** * Constructor function. */ public function __construct() { $this->cache = new SplStack; $this->stmts = new SplStack; } /** * Enter functions. * * @param FunctionLike $node Function * * @return void */ public function enterFunctionLike(FunctionLike $node): void { if (!\preg_match_all('/@memoize \$([\w\d_]+)/', (string) ($node->getDocComment() ?? ''), $matches)) { $this->cache->push(null); $this->stmts->push([]); return; } if ($node->getReturnType() instanceof Identifier && $node->getReturnType()->name === 'void') { throw new \RuntimeException('Cannot memoize void function'); } /** @var Node[] */ $toPrepend = []; /** @var string[] */ $memoizeParams = $matches[1]; /** @var array */ $params = []; foreach ($node->getParams() as $param) { if (!$param->var instanceof Variable) { continue; } $params[$param->var->name] = $param; } /** @var Variable[] */ $memoizeVars = []; foreach ($memoizeParams as $memoizeVar) { if (!isset($params[$memoizeVar])) { throw new \RuntimeException('Cannot find memoization parameter $'.$memoizeVar); } $memoizeParam = $params[$memoizeVar]; if ($memoizeParam->type === null) { throw new \RuntimeException('Cannot memoize by untyped parameter $'.$memoizeVar); } if ($memoizeParam->type instanceof Identifier) { if ($memoizeParam->type->name === 'array') { throw new \RuntimeException('Cannot memoize by array parameter $'.$memoizeVar); } if (\in_array($memoizeParam->type->name, ['string', 'int', 'float', 'bool'])) { $memoizeVars[] = $memoizeParam->var; continue; } } $toPrepend []= Plugin::assign( $variable = new Variable($memoizeParam->var->name.'___memo'), Plugin::call('spl_object_hash', $memoizeParam->var) ); $memoizeVars []= $variable; } $toPrepend []= new Static_([new StaticVar($cache = new Variable('memoizeCache'), new Array_())]); foreach ($memoizeVars as $var) { $cache = new ArrayDimFetch($cache, $var); } $toPrepend []= new If_(new Isset_([$cache]), [new Return_($cache)]); $this->cache->push($cache); $this->stmts->push($toPrepend); } /** * Leave function. * * @param FunctionLike $fun Function * * @return void */ public function leaveFunctionLike(FunctionLike $fun): void { $this->cache->pop(); $fun->stmts = \array_merge($this->stmts->pop(), $fun->stmts); } /** * Enter return expression. * * @param Return_ $return Return expression * * @return void */ public function enterReturn(Return_ $return) { if ($this->cache->top()) { $return->expr = new Assign($this->cache->top(), $return->expr); } } }