|
106 | 106 | use function array_values; |
107 | 107 | use function count; |
108 | 108 | use function in_array; |
| 109 | +use function is_array; |
109 | 110 | use function is_int; |
110 | 111 | use function is_string; |
111 | 112 | use function max; |
@@ -913,6 +914,91 @@ public function getAssignedVariables(Expr $expr): array |
913 | 914 | return []; |
914 | 915 | } |
915 | 916 |
|
| 917 | + private const REPLAYABLE_BODY_ATTRIBUTE = 'convergenceReplayableBody'; |
| 918 | + |
| 919 | + /** |
| 920 | + * Whether a recorded convergence pass over the loop body can replace the |
| 921 | + * final walk. A pass runs at deep statement context, the final walk at top |
| 922 | + * level - constructs that analyse differently between the two (nested |
| 923 | + * loop/label fixpoints run only at top level, statement-level classes are |
| 924 | + * skipped at deep context) disqualify the body. Closure bodies process |
| 925 | + * context-independently and are not traversed. |
| 926 | + * |
| 927 | + * @param Node\Stmt[] $bodyStmts |
| 928 | + */ |
| 929 | + public function isReplayableConvergenceBody(Node $loopNode, array $bodyStmts): bool |
| 930 | + { |
| 931 | + $cached = $loopNode->getAttribute(self::REPLAYABLE_BODY_ATTRIBUTE); |
| 932 | + if ($cached !== null) { |
| 933 | + return $cached; |
| 934 | + } |
| 935 | + |
| 936 | + $replayable = true; |
| 937 | + foreach ($bodyStmts as $bodyStmt) { |
| 938 | + if ($this->hasContextSensitiveConstruct($bodyStmt)) { |
| 939 | + $replayable = false; |
| 940 | + break; |
| 941 | + } |
| 942 | + } |
| 943 | + $loopNode->setAttribute(self::REPLAYABLE_BODY_ATTRIBUTE, $replayable); |
| 944 | + |
| 945 | + return $replayable; |
| 946 | + } |
| 947 | + |
| 948 | + private function hasContextSensitiveConstruct(Node $node): bool |
| 949 | + { |
| 950 | + if ($node instanceof Expr\Closure) { |
| 951 | + return false; |
| 952 | + } |
| 953 | + if ( |
| 954 | + $node instanceof Node\Stmt\While_ |
| 955 | + || $node instanceof Node\Stmt\Do_ |
| 956 | + || $node instanceof Node\Stmt\For_ |
| 957 | + || $node instanceof Foreach_ |
| 958 | + || $node instanceof Node\Stmt\Label |
| 959 | + || $node instanceof Node\Stmt\ClassLike |
| 960 | + ) { |
| 961 | + return true; |
| 962 | + } |
| 963 | + |
| 964 | + foreach ($node->getSubNodeNames() as $subNodeName) { |
| 965 | + $subNode = $node->$subNodeName; |
| 966 | + if ($subNode instanceof Node) { |
| 967 | + if ($this->hasContextSensitiveConstruct($subNode)) { |
| 968 | + return true; |
| 969 | + } |
| 970 | + } elseif (is_array($subNode)) { |
| 971 | + foreach ($subNode as $item) { |
| 972 | + if ($item instanceof Node && $this->hasContextSensitiveConstruct($item)) { |
| 973 | + return true; |
| 974 | + } |
| 975 | + } |
| 976 | + } |
| 977 | + } |
| 978 | + |
| 979 | + return false; |
| 980 | + } |
| 981 | + |
| 982 | + /** |
| 983 | + * Replays a recorded convergence pass's emissions through the real node |
| 984 | + * callback in place of the final loop walk. The pass's storage was merged |
| 985 | + * into $storage by the caller; binding it for the whole replay lets the |
| 986 | + * recorded scopes answer rule asks from the stored before-scopes, the |
| 987 | + * same way the repeated walk's per-emission binding would. |
| 988 | + * |
| 989 | + * @param callable(Node $node, Scope $scope): void $nodeCallback |
| 990 | + */ |
| 991 | + public function replayRecording(RecordingNodeCallback $recording, callable $nodeCallback, ExpressionResultStorage $storage): void |
| 992 | + { |
| 993 | + $stack = $this->getExpressionResultStorageStack(); |
| 994 | + $stack->push($storage); |
| 995 | + try { |
| 996 | + $recording->replayThrough($nodeCallback); |
| 997 | + } finally { |
| 998 | + $stack->pop(); |
| 999 | + } |
| 1000 | + } |
| 1001 | + |
916 | 1002 | /** |
917 | 1003 | * @param callable(Node $node, Scope $scope): void $nodeCallback |
918 | 1004 | */ |
@@ -955,6 +1041,13 @@ public function callNodeCallback( |
955 | 1041 | return; |
956 | 1042 | } |
957 | 1043 |
|
| 1044 | + if ($nodeCallback instanceof RecordingNodeCallback) { |
| 1045 | + // recording never asks about types - the pairs are wrapped and |
| 1046 | + // bound to the storage at replay time instead |
| 1047 | + $nodeCallback($node, $scope); |
| 1048 | + return; |
| 1049 | + } |
| 1050 | + |
958 | 1051 | // post-order emission means the node's own result and every subnode |
959 | 1052 | // result are already stored when the callback fires - NodeCallbackScope |
960 | 1053 | // answers every ask synchronously from the storage; the emitting |
@@ -1126,15 +1219,29 @@ public function processClosureNode( |
1126 | 1219 |
|
1127 | 1220 | $count = 0; |
1128 | 1221 | $closureResultScope = null; |
| 1222 | + $replayBodyRecording = null; |
| 1223 | + $replayPassStorage = null; |
| 1224 | + $replayPassResult = null; |
| 1225 | + $replayEntryScope = null; |
| 1226 | + $bodyIsReplayable = $this->isReplayableConvergenceBody($expr, $expr->stmts); |
1129 | 1227 | do { |
1130 | 1228 | $prevScope = $closureScope; |
1131 | 1229 |
|
1132 | 1230 | $storage = $originalStorage->duplicate(); |
| 1231 | + $bodyRecording = $bodyIsReplayable ? new RecordingNodeCallback() : new NoopNodeCallback(); |
1133 | 1232 | // deep context, like the loop handlers' own convergence passes: inner |
1134 | 1233 | // loops walk single-pass here and only the final walk below (top-level) |
1135 | 1234 | // runs their full convergence - otherwise every closure-convergence |
1136 | 1235 | // pass would re-converge every inner loop from scratch |
1137 | | - $intermediaryClosureScopeResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, new NoopNodeCallback(), StatementContext::createDeep()); |
| 1236 | + $intermediaryClosureScopeResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $bodyRecording, StatementContext::createDeep()); |
| 1237 | + // the candidate to replace the final walk when this pass's entry |
| 1238 | + // turns out to be the fixpoint |
| 1239 | + if ($bodyRecording instanceof RecordingNodeCallback) { |
| 1240 | + $replayBodyRecording = $bodyRecording; |
| 1241 | + $replayPassStorage = $storage; |
| 1242 | + $replayPassResult = $intermediaryClosureScopeResult; |
| 1243 | + $replayEntryScope = $prevScope; |
| 1244 | + } |
1138 | 1245 | $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope(); |
1139 | 1246 | foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) { |
1140 | 1247 | $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope()); |
@@ -1162,7 +1269,24 @@ public function processClosureNode( |
1162 | 1269 | } |
1163 | 1270 |
|
1164 | 1271 | $storage = $originalStorage; |
1165 | | - $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $closureStmtsCallback, StatementContext::createTopLevel()); |
| 1272 | + if ( |
| 1273 | + $replayBodyRecording !== null && $replayPassStorage !== null |
| 1274 | + && $replayPassResult !== null && $replayEntryScope !== null |
| 1275 | + && $closureScope->equals($replayEntryScope) |
| 1276 | + ) { |
| 1277 | + // the final walk would repeat the recorded fixpoint pass exactly |
| 1278 | + // (same entry scope, deterministic walk) - adopt the pass's result |
| 1279 | + // and replay its emissions through the gathering callback instead. |
| 1280 | + // The pass's own entry scope takes over: the recorded pairs carry |
| 1281 | + // its anonymous-function reflection, which the gathering filter |
| 1282 | + // compares by identity (the state is equals-identical anyway). |
| 1283 | + $closureScope = $replayEntryScope; |
| 1284 | + $originalStorage->mergeResults($replayPassStorage); |
| 1285 | + $this->replayRecording($replayBodyRecording, $closureStmtsCallback, $originalStorage); |
| 1286 | + $statementResult = $replayPassResult; |
| 1287 | + } else { |
| 1288 | + $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $closureStmtsCallback, StatementContext::createTopLevel()); |
| 1289 | + } |
1166 | 1290 | $publicStatementResult = $statementResult->toPublic(); |
1167 | 1291 | $closureReturnStatementsNodeScope = $this->refineClosureNodeScope($closureScope, $scope, $expr, $gatheredReturnStatementsWithScope, $gatheredYieldStatementsWithScope, $executionEnds, $statementResult->getThrowPoints(), array_merge($closureImpurePoints, $statementResult->getImpurePoints()), $invalidateExpressions); |
1168 | 1292 | $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode( |
|
0 commit comments