Skip to content

Commit 317da9a

Browse files
committed
Keep statement snapshots' lists in the walk state
A StatementListWalkState snapshot taken before every statement of a body (the two-pass body walk, loop convergence passes) was a clone sharing the list arrays copy-on-write; the walk's next append then copied them, so the snapshot of statement i kept its own copy of i entries of the variable flows, throw points, impure points and dependencies - quadratic memory and time in the body length. A snapshot now counts the entries of its lists, which stay in the state the walk goes on appending to; resuming from a snapshot slices them out, carrying a recorded walk over slices between two snapshots. A label rewrites the exit points, so the body walk goes on with a clone there. Amp-Thread-ID: https://ampcode.com/threads/T-01a1b24f-d997-765a-874f-4ff48d19e6e3
1 parent cbc51a7 commit 317da9a

8 files changed

Lines changed: 4369 additions & 90 deletions

File tree

‎src/Analyser/StatementListWalkState.php‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77

88
/**
99
* The state NodeScopeResolver threads from one statement of a statement list
10-
* to the next. A snapshot (clone) taken before a statement is everything the
11-
* walk needs to continue from that statement again.
10+
* to the next. A snapshot taken before a statement (see
11+
* StatementsHandler::snapshotWalkState()) is everything the walk needs to
12+
* continue from that statement again.
1213
*/
1314
#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/StatementListWalkState.cpp')]
1415
final class StatementListWalkState
@@ -33,6 +34,23 @@ final class StatementListWalkState
3334
/** @var list<Dependencies> */
3435
public array $dependencies = [];
3536

37+
/**
38+
* Set on a snapshot: instead of lists of its own, its lists are the first
39+
* ...Count entries of this state's - the walk's own state, which the walk
40+
* only appends to afterwards.
41+
*/
42+
public ?StatementListWalkState $listsOf = null;
43+
44+
public int $variableFlowCount = 0;
45+
46+
public int $exitPointCount = 0;
47+
48+
public int $throwPointCount = 0;
49+
50+
public int $impurePointCount = 0;
51+
52+
public int $dependencyCount = 0;
53+
3654
public function __construct(public MutatingScope $scope)
3755
{
3856
}

‎src/Analyser/StatementsHandler.php‎

Lines changed: 98 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
use function array_column;
4444
use function array_fill_keys;
4545
use function array_flip;
46-
use function array_key_exists;
4746
use function array_keys;
4847
use function array_merge;
4948
use function array_slice;
@@ -54,6 +53,7 @@
5453
use function is_array;
5554
use function is_int;
5655
use function is_string;
56+
use function max;
5757
use function min;
5858
use function spl_object_id;
5959
use function sprintf;
@@ -509,7 +509,7 @@ private function walkAgainstPreviousPass(
509509
$entries = [];
510510
for ($i = 0; $i < $stmtCount; $i++) {
511511
if (!$finalWalk) {
512-
$entries[$i] = [clone $state, $recording !== null ? $recording->count() : 0];
512+
$entries[$i] = [$this->snapshotWalkState($state), $recording !== null ? $recording->count() : 0];
513513
}
514514
if ($canReplay && ($i > $lastWalked || !$nodeScopeResolver->isContextSensitiveStatement($stmts[$i]))) {
515515
[$recordedEntry, $offset] = $previous[0][$i];
@@ -812,7 +812,12 @@ private function processBodyStmtNodesTwoPass(
812812
$suspendedGatherers = $nodeScopeResolver->suspendNodeGatherers();
813813
try {
814814
foreach ($stmts as $i => $stmt) {
815-
$entries[$i] = [clone $state, $recording->count()];
815+
$entries[$i] = [$this->snapshotWalkState($state), $recording->count()];
816+
if ($stmt instanceof Node\Stmt\Label) {
817+
// the label drops the gotos landing on it from the exit
818+
// points: the snapshots so far keep the lists as they were
819+
$state = clone $state;
820+
}
816821
$this->processStatementStep($nodeScopeResolver, $parentNode, $stmts, $i, $stmt, $state, $storage, $recording, $observationContext, true);
817822
}
818823
} finally {
@@ -866,7 +871,7 @@ private function processBodyStmtNodesTwoPass(
866871
// their recorded effect onto the re-walked scope
867872
$nodeScopeResolver->replayRecordingRange($recording, 0, $entries[$firstSiteStatementIndex][1], $nodeCallback, $storage, $scope);
868873
$hasLabels = $this->containsLabels($stmts);
869-
$state = clone $entries[$firstSiteStatementIndex][0];
874+
$state = $this->resumeWalkState($entries[$firstSiteStatementIndex][0]);
870875
$state->scope = $state->scope->withTemplateArgumentFrame($frame)->withTemplateArgumentConstraints(null);
871876
for ($i = $firstSiteStatementIndex; $i < $stmtCount; $i++) {
872877
[$recordedEntry, $offset] = $entries[$i];
@@ -987,7 +992,7 @@ private function inferStaticVariableTypes(
987992
}
988993
$frame = (new TemplateArgumentFrame($parentFrame, closureSignatureBody: $parentNode, closureSignatureStmts: $stmts))->withStaticVariableTypes($siteTypes, $staticStatementIndexes, $conditionalExpressions);
989994

990-
$state = clone $entries[$start][0];
995+
$state = $this->resumeWalkState($entries[$start][0]);
991996
$state->scope = $state->scope->withTemplateArgumentFrame($frame);
992997
$walkStorage = $storage->duplicate();
993998
$walkRecording = new RecordingNodeCallback();
@@ -1630,7 +1635,7 @@ private function observeClosureSignatures(
16301635
): TemplateArgumentFrame
16311636
{
16321637
$start = $frame->firstSiteStatementIndex() ?? 0;
1633-
$state = clone $entries[$start][0];
1638+
$state = $this->resumeWalkState($entries[$start][0]);
16341639
$state->scope = $state->scope->withTemplateArgumentFrame($frame);
16351640
// the second pass replays the observation pass's recording against the storage
16361641
$storage = $storage->duplicate();
@@ -1715,22 +1720,98 @@ private function containsLabels(array $stmts): bool
17151720
return false;
17161721
}
17171722

1718-
/** Carries what the recorded walk from $from to $to added onto $state. */
1719-
private function appendRecordedStatementResults(StatementListWalkState $state, StatementListWalkState $from, StatementListWalkState $to): void
1723+
/**
1724+
* The state before the statement $state walks next. Its lists stay in
1725+
* $state, which the walk only appends to afterwards: a copy of them in
1726+
* every snapshot would make the snapshots of a body take space quadratic
1727+
* in its length. A label rewrites the exit points - the body walk goes on
1728+
* with a clone from there, and a loop pass never replays a body with
1729+
* labels.
1730+
*/
1731+
private function snapshotWalkState(StatementListWalkState $state): StatementListWalkState
17201732
{
1721-
foreach ($to->variableFlows as $index => $flow) {
1722-
if (array_key_exists($index, $from->variableFlows)) {
1723-
continue;
1724-
}
1733+
$snapshot = new StatementListWalkState($state->scope);
1734+
$snapshot->alreadyTerminated = $state->alreadyTerminated;
1735+
$snapshot->hasYield = $state->hasYield;
1736+
$snapshot->listsOf = $state;
1737+
$snapshot->variableFlowCount = count($state->variableFlows);
1738+
$snapshot->exitPointCount = count($state->exitPoints);
1739+
$snapshot->throwPointCount = count($state->throwPoints);
1740+
$snapshot->impurePointCount = count($state->impurePoints);
1741+
$snapshot->dependencyCount = count($state->dependencies);
1742+
1743+
return $snapshot;
1744+
}
17251745

1726-
$state->variableFlows[$index] = $flow;
1746+
/** A state with lists of its own to walk on from $entry. */
1747+
private function resumeWalkState(StatementListWalkState $entry): StatementListWalkState
1748+
{
1749+
$lists = $entry->listsOf;
1750+
if ($lists === null) {
1751+
return clone $entry;
1752+
}
1753+
1754+
$state = new StatementListWalkState($entry->scope);
1755+
$state->alreadyTerminated = $entry->alreadyTerminated;
1756+
$state->hasYield = $entry->hasYield;
1757+
$state->variableFlows = array_slice($lists->variableFlows, 0, $entry->variableFlowCount, true);
1758+
$state->exitPoints = array_slice($lists->exitPoints, 0, $entry->exitPointCount);
1759+
$state->throwPoints = array_slice($lists->throwPoints, 0, $entry->throwPointCount);
1760+
$state->impurePoints = array_slice($lists->impurePoints, 0, $entry->impurePointCount);
1761+
$state->dependencies = array_slice($lists->dependencies, 0, $entry->dependencyCount);
1762+
1763+
return $state;
1764+
}
1765+
1766+
/**
1767+
* The lengths of the variable flow, exit point, throw point, impure point
1768+
* and dependency lists of $state.
1769+
*
1770+
* @return array{int, int, int, int, int}
1771+
*/
1772+
private function getWalkStateListCounts(StatementListWalkState $state): array
1773+
{
1774+
if ($state->listsOf !== null) {
1775+
return [$state->variableFlowCount, $state->exitPointCount, $state->throwPointCount, $state->impurePointCount, $state->dependencyCount];
17271776
}
1777+
1778+
return [count($state->variableFlows), count($state->exitPoints), count($state->throwPoints), count($state->impurePoints), count($state->dependencies)];
1779+
}
1780+
1781+
/**
1782+
* Carries what the recorded walk from $from to $to added onto $state. The
1783+
* lists of $from are the first entries of the lists of $to. A snapshot
1784+
* $state only counts the added entries in: the state holding its lists
1785+
* has to receive them.
1786+
*/
1787+
private function appendRecordedStatementResults(StatementListWalkState $state, StatementListWalkState $from, StatementListWalkState $to): void
1788+
{
1789+
[$fromFlows, $fromExitPoints, $fromThrowPoints, $fromImpurePoints, $fromDependencies] = $this->getWalkStateListCounts($from);
1790+
[$toFlows, $toExitPoints, $toThrowPoints, $toImpurePoints, $toDependencies] = $this->getWalkStateListCounts($to);
17281791
$state->hasYield = $state->hasYield || ($to->hasYield && !$from->hasYield);
17291792
$state->alreadyTerminated = $state->alreadyTerminated || ($to->alreadyTerminated && !$from->alreadyTerminated);
1730-
$state->exitPoints = array_merge($state->exitPoints, array_slice($to->exitPoints, count($from->exitPoints)));
1731-
$state->throwPoints = array_merge($state->throwPoints, array_slice($to->throwPoints, count($from->throwPoints)));
1732-
$state->impurePoints = array_merge($state->impurePoints, array_slice($to->impurePoints, count($from->impurePoints)));
1733-
$state->dependencies = array_merge($state->dependencies, array_slice($to->dependencies, count($from->dependencies)));
1793+
$addedFlows = max(0, $toFlows - $fromFlows);
1794+
$addedExitPoints = max(0, $toExitPoints - $fromExitPoints);
1795+
$addedThrowPoints = max(0, $toThrowPoints - $fromThrowPoints);
1796+
$addedImpurePoints = max(0, $toImpurePoints - $fromImpurePoints);
1797+
$addedDependencies = max(0, $toDependencies - $fromDependencies);
1798+
if ($state->listsOf !== null) {
1799+
$state->variableFlowCount += $addedFlows;
1800+
$state->exitPointCount += $addedExitPoints;
1801+
$state->throwPointCount += $addedThrowPoints;
1802+
$state->impurePointCount += $addedImpurePoints;
1803+
$state->dependencyCount += $addedDependencies;
1804+
return;
1805+
}
1806+
1807+
$lists = $to->listsOf ?? $to;
1808+
foreach (array_slice($lists->variableFlows, $fromFlows, $addedFlows, true) as $index => $flow) {
1809+
$state->variableFlows[$index] = $flow;
1810+
}
1811+
$state->exitPoints = array_merge($state->exitPoints, array_slice($lists->exitPoints, $fromExitPoints, $addedExitPoints));
1812+
$state->throwPoints = array_merge($state->throwPoints, array_slice($lists->throwPoints, $fromThrowPoints, $addedThrowPoints));
1813+
$state->impurePoints = array_merge($state->impurePoints, array_slice($lists->impurePoints, $fromImpurePoints, $addedImpurePoints));
1814+
$state->dependencies = array_merge($state->dependencies, array_slice($lists->dependencies, $fromDependencies, $addedDependencies));
17341815
}
17351816

17361817
public function getVariableMentionFlow(Node\Stmt $stmt): ?VariableFlow
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace TemplateArgumentGotoLabel;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/** @template T */
8+
class Collection
9+
{
10+
11+
/** @param list<T> $items */
12+
public function __construct(array $items)
13+
{
14+
}
15+
16+
}
17+
18+
/** @param Collection<int> $collection */
19+
function takeInts(Collection $collection): void
20+
{
21+
}
22+
23+
function gotoBeforeSite(bool $b): int
24+
{
25+
$x = 1;
26+
if ($b) {
27+
$x = 2;
28+
goto end;
29+
}
30+
$collection = new Collection([1]);
31+
takeInts($collection);
32+
assertType('TemplateArgumentGotoLabel\Collection<int>', $collection);
33+
$x = 3;
34+
end:
35+
assertType('2|3', $x);
36+
return $x;
37+
}

0 commit comments

Comments
 (0)