Skip to content

Commit 67acd19

Browse files
committed
Decide whether a closure is assigned to its own by-ref variable in one walk
The check looked the closure up in a per-frame map of every closure assigned in the body, nested function-likes included: a linear search per question over all closures assigned to the variable, and each enclosing frame of a nested closure walking the same subtree again. The walk now marks the closures themselves, so each closure is decided once, by the first walk that reaches it, and getAssignedClosures() is back to the top-level map findAssignedClosures() needs. Amp-Thread-ID: https://ampcode.com/threads/T-01a19c29-188b-7572-9730-387584a8a9fd
1 parent b958efe commit 67acd19

3 files changed

Lines changed: 170 additions & 52 deletions

File tree

‎src/Analyser/Generics/ClosureSignatureInference.php‎

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use function count;
3232
use function in_array;
3333
use function is_array;
34+
use function is_bool;
3435
use function is_string;
3536
use function spl_object_id;
3637
use function str_starts_with;
@@ -91,7 +92,7 @@ final class ClosureSignatureInference
9192

9293
private const REBOUND_VARIABLES_ATTRIBUTE = 'closureSignatureReboundVariables';
9394

94-
private const NESTED_ASSIGNED_CLOSURES_ATTRIBUTE = 'closureSignatureNestedAssignedClosures';
95+
private const ASSIGNED_TO_BY_REF_USE_ATTRIBUTE = 'closureSignatureAssignedToByRefUse';
9596

9697
public function __construct(
9798
#[AutowiredParameter(ref: '%featureToggles.closureSignaturesFromUsages%')]
@@ -147,19 +148,18 @@ public function getByRefUseMarkers(MutatingScope $scope, Closure|ArrowFunction $
147148
return [];
148149
}
149150
$reboundNames = self::getReboundVariableNames($body, $frame->getClosureSignatureStmts());
150-
$assignedClosures = self::getAssignedClosures($body, $frame->getClosureSignatureStmts(), true);
151+
if (self::isAssignedToByRefUse($expr, $frame->getClosureSignatureStmts())) {
152+
// `$f = function () use (&$f)`: the body reaches the closure itself
153+
// through the variable - aliased, captured, passed on - so its
154+
// invocations cannot all be followed
155+
return [];
156+
}
151157
foreach ($names as $name) {
152158
// unset or bound to another reference, the variable no longer is the
153159
// one the closure writes to - its effects stay the creation-time fixpoint
154160
if (isset($reboundNames[$name])) {
155161
return [];
156162
}
157-
// `$f = function () use (&$f)`: the body reaches the closure itself
158-
// through the variable - aliased, captured, passed on - so its
159-
// invocations cannot all be followed
160-
if (in_array($expr, $assignedClosures[$name] ?? [], true)) {
161-
return [];
162-
}
163163
}
164164

165165
$markers = [];
@@ -1161,17 +1161,13 @@ public function findAssignedClosures(MutatingScope $scope, string $name): array
11611161
}
11621162

11631163
/**
1164-
* With $nested, also the closures assigned in the function-likes the body
1165-
* contains - to the variables of those.
1166-
*
11671164
* @param Node\Stmt[] $stmts
11681165
* @return array<string, list<Closure|ArrowFunction>>
11691166
*/
1170-
private static function getAssignedClosures(Node $body, array $stmts, bool $nested = false): array
1167+
private static function getAssignedClosures(Node $body, array $stmts): array
11711168
{
1172-
$attribute = $nested ? self::NESTED_ASSIGNED_CLOSURES_ATTRIBUTE : self::ASSIGNED_CLOSURES_ATTRIBUTE;
11731169
/** @var array<string, list<Closure|ArrowFunction>>|null $cached */
1174-
$cached = $body->getAttribute($attribute);
1170+
$cached = $body->getAttribute(self::ASSIGNED_CLOSURES_ATTRIBUTE);
11751171
if ($cached !== null) {
11761172
return $cached;
11771173
}
@@ -1180,7 +1176,7 @@ private static function getAssignedClosures(Node $body, array $stmts, bool $nest
11801176
$stack = $stmts;
11811177
while (count($stack) > 0) {
11821178
$node = array_pop($stack);
1183-
if (!$nested && ($node instanceof Node\FunctionLike || $node instanceof Node\Stmt\ClassLike)) {
1179+
if ($node instanceof Node\FunctionLike || $node instanceof Node\Stmt\ClassLike) {
11841180
continue;
11851181
}
11861182
if (
@@ -1205,7 +1201,63 @@ private static function getAssignedClosures(Node $body, array $stmts, bool $nest
12051201
}
12061202
}
12071203
}
1208-
$body->setAttribute($attribute, $assigned);
1204+
$body->setAttribute(self::ASSIGNED_CLOSURES_ATTRIBUTE, $assigned);
1205+
1206+
return $assigned;
1207+
}
1208+
1209+
/**
1210+
* Whether the closure is assigned to one of its by-ref variables. The first
1211+
* question about a closure in the statements decides it for every closure
1212+
* in them, nested function-likes included - a closure created in another
1213+
* closure's body is observed through the enclosing body's frame - so each
1214+
* closure is decided by one walk.
1215+
*
1216+
* @param Node\Stmt[] $stmts
1217+
*/
1218+
private static function isAssignedToByRefUse(Closure $expr, array $stmts): bool
1219+
{
1220+
$assigned = $expr->getAttribute(self::ASSIGNED_TO_BY_REF_USE_ATTRIBUTE);
1221+
if (is_bool($assigned)) {
1222+
return $assigned;
1223+
}
1224+
1225+
$stack = $stmts;
1226+
while (count($stack) > 0) {
1227+
$node = array_pop($stack);
1228+
if (
1229+
$node instanceof Expr\Assign
1230+
&& $node->var instanceof Expr\Variable
1231+
&& is_string($node->var->name)
1232+
&& $node->expr instanceof Closure
1233+
) {
1234+
foreach ($node->expr->uses as $use) {
1235+
if ($use->byRef && $use->var->name === $node->var->name) {
1236+
$node->expr->setAttribute(self::ASSIGNED_TO_BY_REF_USE_ATTRIBUTE, true);
1237+
break;
1238+
}
1239+
}
1240+
} elseif ($node instanceof Closure && $node->getAttribute(self::ASSIGNED_TO_BY_REF_USE_ATTRIBUTE) === null) {
1241+
// an assignment is visited before the closure it assigns
1242+
$node->setAttribute(self::ASSIGNED_TO_BY_REF_USE_ATTRIBUTE, false);
1243+
}
1244+
foreach ($node->getSubNodeNames() as $subNodeName) {
1245+
$subNode = $node->$subNodeName;
1246+
if ($subNode instanceof Node) {
1247+
$stack[] = $subNode;
1248+
} elseif (is_array($subNode)) {
1249+
foreach ($subNode as $item) {
1250+
if (!$item instanceof Node) {
1251+
continue;
1252+
}
1253+
$stack[] = $item;
1254+
}
1255+
}
1256+
}
1257+
}
1258+
1259+
$assigned = $expr->getAttribute(self::ASSIGNED_TO_BY_REF_USE_ATTRIBUTE) === true;
1260+
$expr->setAttribute(self::ASSIGNED_TO_BY_REF_USE_ATTRIBUTE, $assigned);
12091261

12101262
return $assigned;
12111263
}

‎turbo-ext/src/ClosureSignatureInference.cpp‎

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ zend_string *pt_csi_return_template_name = nullptr;
3030
zend_string *pt_csi_closed_body_attribute = nullptr;
3131
zend_string *pt_csi_returns_context_typed_attribute = nullptr;
3232
zend_string *pt_csi_assigned_closures_attribute = nullptr;
33-
zend_string *pt_csi_nested_assigned_closures_attribute = nullptr;
33+
zend_string *pt_csi_assigned_to_by_ref_use_attribute = nullptr;
3434
/* the by-ref follow-up's template names and attribute names */
3535
zend_string *pt_csi_invocation_template_name = nullptr;
3636
zend_string *pt_csi_by_ref_uses_attribute = nullptr;
@@ -231,10 +231,9 @@ void containsMarkerBody(zval *contains, zval *state1, uint32_t argc, zval *argv,
231231
}
232232

233233
/* Mirrors the private static getAssignedClosures(); UNDEF = pending exception */
234-
zv::Val getAssignedClosures(zval *body, zval *stmts, bool nested = false)
234+
zv::Val getAssignedClosures(zval *body, zval *stmts)
235235
{
236-
zend_string *attribute = nested ? pt_csi_nested_assigned_closures_attribute : pt_csi_assigned_closures_attribute;
237-
zv::Val cached = pt_engine_node_get_attribute(Z_OBJ_P(body), ZSTR_VAL(attribute), ZSTR_LEN(attribute));
236+
zv::Val cached = pt_engine_node_get_attribute(Z_OBJ_P(body), ZSTR_VAL(pt_csi_assigned_closures_attribute), ZSTR_LEN(pt_csi_assigned_closures_attribute));
238237
if (UNEXPECTED(cached.isUndef())) return zv::Val();
239238
if (!cached.isNull()) return cached;
240239

@@ -248,17 +247,15 @@ zv::Val getAssignedClosures(zval *body, zval *stmts, bool nested = false)
248247
for (;;) {
249248
zv::Val node = popNode(stack);
250249
if (node.isUndef()) break;
251-
if (!nested) {
252-
bool skip;
253-
if (UNEXPECTED(!isA(node.raw(), PT_CLASS_FUNCTION_LIKE, skip))) return zv::Val();
254-
if (!skip && UNEXPECTED(!isA(node.raw(), PT_CLASS_CLASS_LIKE_STMT, skip))) return zv::Val();
255-
if (skip) continue;
256-
}
250+
bool skip;
251+
if (UNEXPECTED(!isA(node.raw(), PT_CLASS_FUNCTION_LIKE, skip))) return zv::Val();
252+
if (!skip && UNEXPECTED(!isA(node.raw(), PT_CLASS_CLASS_LIKE_STMT, skip))) return zv::Val();
253+
if (skip) continue;
257254
if (UNEXPECTED(!collectAssignedClosure(node.raw(), assigned))) return zv::Val();
258255
if (UNEXPECTED(!pushSubNodes(node.raw(), stack))) return zv::Val();
259256
}
260257
zv::Val result(std::move(assigned));
261-
if (UNEXPECTED(!pt_engine_node_set_attribute(Z_OBJ_P(body), ZSTR_VAL(attribute), ZSTR_LEN(attribute), result.raw()))) return zv::Val();
258+
if (UNEXPECTED(!pt_engine_node_set_attribute(Z_OBJ_P(body), ZSTR_VAL(pt_csi_assigned_closures_attribute), ZSTR_LEN(pt_csi_assigned_closures_attribute), result.raw()))) return zv::Val();
262259
return result;
263260
}
264261

@@ -577,6 +574,81 @@ zv::Val anonymousMarker(zval *site, zend_string *name, zend_long variance, zval
577574
return true;
578575
}
579576

577+
/* Mirrors the private static isAssignedToByRefUse(); false = pending exception */
578+
[[nodiscard]] bool isAssignedToByRefUse(zval *expr, zval *stmts, bool &out)
579+
{
580+
const char *attribute = ZSTR_VAL(pt_csi_assigned_to_by_ref_use_attribute);
581+
size_t attributeLength = ZSTR_LEN(pt_csi_assigned_to_by_ref_use_attribute);
582+
zv::Val cached = pt_engine_node_get_attribute(Z_OBJ_P(expr), attribute, attributeLength);
583+
if (UNEXPECTED(cached.isUndef())) return false;
584+
if (Z_TYPE_P(cached.raw()) == IS_TRUE || Z_TYPE_P(cached.raw()) == IS_FALSE) {
585+
out = Z_TYPE_P(cached.raw()) == IS_TRUE;
586+
return true;
587+
}
588+
589+
zval marked;
590+
zv::Arr stack = zv::Arr::create(0);
591+
if (Z_TYPE_P(stmts) == IS_ARRAY) {
592+
for (auto entry : zv::ArrRef(stmts)) {
593+
stack.push(zv::Val::copyOf(entry.value().deref()));
594+
}
595+
}
596+
for (;;) {
597+
zv::Val node = popNode(stack);
598+
if (node.isUndef()) break;
599+
zval *n = node.raw();
600+
bool is;
601+
if (UNEXPECTED(!isA(n, PT_CLASS_ASSIGN_EXPR, is))) return false;
602+
if (is) {
603+
zval *var = read(pt_csi_assign_var_site, n, PT_LC("var"));
604+
if (UNEXPECTED(var == NULL)) return false;
605+
zend_string *name;
606+
if (UNEXPECTED(!variableStringName(pt_csi_variable_name_site, var, name))) return false;
607+
zval *value = read(pt_csi_assign_expr_site, n, PT_LC("expr"));
608+
if (UNEXPECTED(value == NULL)) return false;
609+
bool isClosure;
610+
if (UNEXPECTED(!isA(value, PT_CLASS_CLOSURE_EXPR, isClosure))) return false;
611+
if (name != NULL && isClosure) {
612+
zval *uses = read(pt_csi_by_ref_uses_site, value, PT_LC("uses"));
613+
if (UNEXPECTED(uses == NULL)) return false;
614+
zv::Val usesHold = zv::Val::copyOf(zv::Ref(uses));
615+
for (auto entry : zv::ArrRef(usesHold.raw())) {
616+
zval *use = entry.value().deref().raw();
617+
zval *byRef = read(pt_csi_by_ref_use_by_ref_site, use, PT_LC("byRef"));
618+
if (UNEXPECTED(byRef == NULL)) return false;
619+
if (!zend_is_true(byRef)) continue;
620+
zval *useVar = read(pt_csi_by_ref_use_var_site, use, PT_LC("var"));
621+
if (UNEXPECTED(useVar == NULL)) return false;
622+
zval *useName = read(pt_csi_by_ref_variable_name_site, useVar, PT_LC("name"));
623+
if (UNEXPECTED(useName == NULL)) return false;
624+
if (Z_TYPE_P(useName) != IS_STRING || !zend_string_equals(Z_STR_P(useName), name)) continue;
625+
ZVAL_TRUE(&marked);
626+
if (UNEXPECTED(!pt_engine_node_set_attribute(Z_OBJ_P(value), attribute, attributeLength, &marked))) return false;
627+
break;
628+
}
629+
}
630+
} else {
631+
if (UNEXPECTED(!isA(n, PT_CLASS_CLOSURE_EXPR, is))) return false;
632+
if (is) {
633+
zv::Val decided = pt_engine_node_get_attribute(Z_OBJ_P(n), attribute, attributeLength);
634+
if (UNEXPECTED(decided.isUndef())) return false;
635+
if (decided.isNull()) {
636+
// an assignment is visited before the closure it assigns
637+
ZVAL_FALSE(&marked);
638+
if (UNEXPECTED(!pt_engine_node_set_attribute(Z_OBJ_P(n), attribute, attributeLength, &marked))) return false;
639+
}
640+
}
641+
}
642+
if (UNEXPECTED(!pushSubNodes(n, stack))) return false;
643+
}
644+
645+
zv::Val decided = pt_engine_node_get_attribute(Z_OBJ_P(expr), attribute, attributeLength);
646+
if (UNEXPECTED(decided.isUndef())) return false;
647+
out = Z_TYPE_P(decided.raw()) == IS_TRUE;
648+
ZVAL_BOOL(&marked, out);
649+
return pt_engine_node_set_attribute(Z_OBJ_P(expr), attribute, attributeLength, &marked);
650+
}
651+
580652
/* Mirrors the private bodyYieldsOrInvokes(); false = pending exception */
581653
[[nodiscard]] bool bodyYieldsOrInvokes(zval *stmts, zval *names, bool &out, bool checkYield = true)
582654
{
@@ -1077,8 +1149,12 @@ class ClosureSignatureInference
10771149
if (UNEXPECTED(stmts.isUndef())) return zv::Val();
10781150
zv::Val reboundNames = getReboundVariableNames(body.raw(), stmts.raw());
10791151
if (UNEXPECTED(reboundNames.isUndef())) return zv::Val();
1080-
zv::Val assignedClosures = getAssignedClosures(body.raw(), stmts.raw(), true);
1081-
if (UNEXPECTED(assignedClosures.isUndef())) return zv::Val();
1152+
bool assignedToByRefUse = false;
1153+
if (UNEXPECTED(!isAssignedToByRefUse(expr, stmts.raw(), assignedToByRefUse))) return zv::Val();
1154+
// `$f = function () use (&$f)`: the body reaches the closure itself
1155+
// through the variable - aliased, captured, passed on - so its
1156+
// invocations cannot all be followed
1157+
if (assignedToByRefUse) return zv::Val(zv::Arr::empty());
10821158
for (auto entry : zv::ArrRef(names.raw())) {
10831159
zval *name = entry.value().deref().raw();
10841160
if (Z_TYPE_P(name) != IS_STRING) continue;
@@ -1087,18 +1163,6 @@ class ClosureSignatureInference
10871163
if (zend_symtable_find(Z_ARRVAL_P(reboundNames.raw()), Z_STR_P(name)) != NULL) {
10881164
return zv::Val(zv::Arr::empty());
10891165
}
1090-
// `$f = function () use (&$f)`: the body reaches the closure itself
1091-
// through the variable - aliased, captured, passed on - so its
1092-
// invocations cannot all be followed
1093-
zval *closures = zend_symtable_find(Z_ARRVAL_P(assignedClosures.raw()), Z_STR_P(name));
1094-
if (closures != NULL && Z_TYPE_P(closures) == IS_ARRAY) {
1095-
for (auto closure : zv::ArrRef(closures)) {
1096-
zval *candidate = closure.value().deref().raw();
1097-
if (Z_TYPE_P(candidate) == IS_OBJECT && Z_OBJ_P(candidate) == Z_OBJ_P(expr)) {
1098-
return zv::Val(zv::Arr::empty());
1099-
}
1100-
}
1101-
}
11021166
}
11031167

11041168
zv::Arr markers = zv::Arr::empty();
@@ -2002,7 +2066,7 @@ PT_MINIT_REGISTRATION(pt_register_closure_signature_inference)
20022066
pt_csi_closed_body_attribute = zend_string_init_interned(PT_LC("closureSignatureClosedBody"), 1);
20032067
pt_csi_returns_context_typed_attribute = zend_string_init_interned(PT_LC("closureSignatureReturnsContextTyped"), 1);
20042068
pt_csi_assigned_closures_attribute = zend_string_init_interned(PT_LC("closureSignatureAssignedClosures"), 1);
2005-
pt_csi_nested_assigned_closures_attribute = zend_string_init_interned(PT_LC("closureSignatureNestedAssignedClosures"), 1);
2069+
pt_csi_assigned_to_by_ref_use_attribute = zend_string_init_interned(PT_LC("closureSignatureAssignedToByRefUse"), 1);
20062070
pt_csi_invocation_template_name = zend_string_init_interned(PT_LC("@invokes"), 1);
20072071
pt_csi_by_ref_uses_attribute = zend_string_init_interned(PT_LC("closureSignatureByRefUses"), 1);
20082072
pt_csi_arrow_function_outer_variables_attribute = zend_string_init_interned(PT_LC("closureSignatureArrowFunctionOuterVariables"), 1);
@@ -2020,7 +2084,7 @@ PT_MINIT_REGISTRATION(pt_register_closure_signature_inference)
20202084
cls.privateClassConstantString("CLOSED_BODY_ATTRIBUTE", "closureSignatureClosedBody");
20212085
cls.privateClassConstantString("ASSIGNED_CLOSURES_ATTRIBUTE", "closureSignatureAssignedClosures");
20222086
cls.privateClassConstantString("REBOUND_VARIABLES_ATTRIBUTE", "closureSignatureReboundVariables");
2023-
cls.privateClassConstantString("NESTED_ASSIGNED_CLOSURES_ATTRIBUTE", "closureSignatureNestedAssignedClosures");
2087+
cls.privateClassConstantString("ASSIGNED_TO_BY_REF_USE_ATTRIBUTE", "closureSignatureAssignedToByRefUse");
20242088

20252089
/* the real parameter types: the DI container autowires the service by
20262090
* reflecting the constructor */

‎turbo-ext/src/generated/ClosureSignatureInference.h‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,10 @@ inline constexpr char strings[] =
106106
"scanClosedBody\0" /* 1356 */
107107
"infersInvocationReturnType\0" /* 1371 */
108108
"findAssignedClosures\0" /* 1398 */
109-
"nested\0" /* 1419 */
110-
"false\0" /* 1426 */
111-
"getAssignedClosures\0" /* 1432 */
112-
"collectTargetNames\0" /* 1452 */
113-
"isContextTyped"; /* 1471 */
109+
"getAssignedClosures\0" /* 1419 */
110+
"isAssignedToByRefUse\0" /* 1439 */
111+
"collectTargetNames\0" /* 1460 */
112+
"isContextTyped"; /* 1479 */
114113
inline constexpr reg::PackedArg args[] = {
115114
reg::packed(0, MAY_BE_BOOL), /* __construct $enabled */
116115
reg::packed(20, 0, 27), /* isClosureSignatureMarker $marker */
@@ -217,8 +216,10 @@ inline constexpr reg::PackedArg args[] = {
217216
reg::packed(79, MAY_BE_ARRAY), /* findAssignedClosures return */
218217
reg::packed(884, 0, 889), /* getAssignedClosures $body */
219218
reg::packed(836, MAY_BE_ARRAY), /* getAssignedClosures $stmts */
220-
reg::packed(1419, MAY_BE_BOOL, reg::NoString, false, false, 1426), /* getAssignedClosures $nested */
221219
reg::packed(79, MAY_BE_ARRAY), /* getAssignedClosures return */
220+
reg::packed(171, 0, 257), /* isAssignedToByRefUse $expr */
221+
reg::packed(836, MAY_BE_ARRAY), /* isAssignedToByRefUse $stmts */
222+
reg::packed(79, MAY_BE_BOOL), /* isAssignedToByRefUse return */
222223
reg::packed(953, 0, 649), /* collectTargetNames $target */
223224
reg::packed(842, MAY_BE_ARRAY, reg::NoString, true, false), /* collectTargetNames $names */
224225
reg::packed(79, MAY_BE_VOID), /* collectTargetNames return */
@@ -268,9 +269,10 @@ inline constexpr sigtab::Sig isClosedBody = { { 1343 /* isClosedBody */, 2, 91,
268269
inline constexpr sigtab::Sig scanClosedBody = { { 1356 /* scanClosedBody */, 2, 94, 2, 96, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
269270
inline constexpr sigtab::Sig infersInvocationReturnType = { { 1371 /* infersInvocationReturnType */, 2, 97, 2, 99, ZEND_ACC_PUBLIC } };
270271
inline constexpr sigtab::Sig findAssignedClosures = { { 1398 /* findAssignedClosures */, 2, 100, 2, 102, ZEND_ACC_PUBLIC } };
271-
inline constexpr sigtab::Sig getAssignedClosures = { { 1432 /* getAssignedClosures */, 2, 103, 3, 106, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
272-
inline constexpr sigtab::Sig collectTargetNames = { { 1452 /* collectTargetNames */, 2, 107, 2, 109, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
273-
inline constexpr sigtab::Sig isContextTyped = { { 1471 /* isContextTyped */, 1, 110, 1, 111, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
272+
inline constexpr sigtab::Sig getAssignedClosures = { { 1419 /* getAssignedClosures */, 2, 103, 2, 105, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
273+
inline constexpr sigtab::Sig isAssignedToByRefUse = { { 1439 /* isAssignedToByRefUse */, 2, 106, 2, 108, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
274+
inline constexpr sigtab::Sig collectTargetNames = { { 1460 /* collectTargetNames */, 2, 109, 2, 111, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
275+
inline constexpr sigtab::Sig isContextTyped = { { 1479 /* isContextTyped */, 1, 112, 1, 113, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
274276
} // namespace sig
275277

276278
} // namespace ptdecl::ClosureSignatureInference

0 commit comments

Comments
 (0)