Skip to content

Commit 93e2cd9

Browse files
committed
Implement Scope Functions
fn() { /* body */ } Signed-off-by: Bob Weinand <bobwei9@hotmail.com>
1 parent 0173ef4 commit 93e2cd9

108 files changed

Lines changed: 5719 additions & 1179 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Zend/Optimizer/block_pass.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,8 @@ static void zend_optimize_block(zend_basic_block *block, zend_op_array *op_array
942942
src->opcode != ZEND_ADD_ARRAY_ELEMENT &&
943943
src->opcode != ZEND_ADD_ARRAY_UNPACK &&
944944
(src->opcode != ZEND_DECLARE_LAMBDA_FUNCTION ||
945+
src == opline -1) &&
946+
(src->opcode != ZEND_DECLARE_SCOPE_FUNC ||
945947
src == opline -1)) {
946948
src->result.var = opline->result.var;
947949
VAR_SOURCE(opline->op1) = NULL;

Zend/Optimizer/compact_literals.c

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,18 @@
3535

3636
typedef struct _literal_info {
3737
uint8_t num_related;
38+
bool pinned;
3839
} literal_info;
3940

4041
#define LITERAL_INFO(n, related) do { \
4142
info[n].num_related = (related); \
4243
} while (0)
4344

45+
#define LITERAL_INFO_PIN(n) do { \
46+
info[n].num_related = 1; \
47+
info[n].pinned = true; \
48+
} while (0)
49+
4450
static uint32_t add_static_slot(HashTable *hash,
4551
zend_op_array *op_array,
4652
uint32_t op1,
@@ -239,6 +245,14 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
239245
}
240246
}
241247
break;
248+
case ZEND_ENTER_SCOPE_FUNC: {
249+
/* We must preserve the precise ordering of the scope fn literals mapping to CV offsets. */
250+
uint32_t num_params = opline->op1.num;
251+
for (uint32_t k = 0; k < num_params; k++) {
252+
LITERAL_INFO_PIN(k);
253+
}
254+
break;
255+
}
242256
default:
243257
if (opline->op1_type == IS_CONST) {
244258
LITERAL_INFO(opline->op1.constant, 1);
@@ -315,7 +329,14 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
315329
map[i] = l_true;
316330
break;
317331
case IS_LONG:
318-
if (info[i].num_related == 1) {
332+
if (info[i].pinned) {
333+
map[i] = j;
334+
if (i != j) {
335+
op_array->literals[j] = op_array->literals[i];
336+
info[j] = info[i];
337+
}
338+
j++;
339+
} else if (info[i].num_related == 1) {
319340
if ((pos = zend_hash_index_find(&hash, Z_LVAL(op_array->literals[i]))) != NULL) {
320341
map[i] = Z_LVAL_P(pos);
321342
} else {

Zend/Optimizer/compact_vars.c

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,58 @@
1818
#include "zend_bitset.h"
1919
#include "zend_observer.h"
2020

21+
/* Recursively include scope fns for shared CVs; apply changes with vars_map, otherwise fill used_vars */
22+
static void scope_fn_visit_parent_cvs(zend_op_array *scope_op, zend_bitset used_vars, const uint32_t *vars_map)
23+
{
24+
#define VISIT_CV(slot) if (slot##_type == IS_CV) { \
25+
uint32_t old = VAR_NUM(slot.var); \
26+
if (vars_map) slot.var = NUM_VAR(vars_map[old]); \
27+
else zend_bitset_incl(used_vars, old); \
28+
}
29+
bool in_body = false;
30+
for (uint32_t i = 0; i < scope_op->last; i++) {
31+
zend_op *opline = &scope_op->opcodes[i];
32+
if (opline->opcode == ZEND_ENTER_SCOPE_FUNC) {
33+
in_body = true;
34+
/* Mapping is pinned at literals[0..num_params). */
35+
uint32_t num_params = opline->op1.num;
36+
for (uint32_t j = 0; j < num_params; j++) {
37+
zval *zv = &scope_op->literals[j];
38+
uint32_t off = (uint32_t)Z_LVAL_P(zv);
39+
if (vars_map) {
40+
ZVAL_LONG(zv, NUM_VAR(vars_map[VAR_NUM(off)]));
41+
} else {
42+
zend_bitset_incl(used_vars, VAR_NUM(off));
43+
}
44+
}
45+
continue;
46+
}
47+
if (!in_body) {
48+
continue;
49+
}
50+
VISIT_CV(opline->op1);
51+
VISIT_CV(opline->op2);
52+
VISIT_CV(opline->result);
53+
}
54+
for (uint32_t i = 0; i < scope_op->num_dynamic_func_defs; i++) {
55+
zend_op_array *nested = scope_op->dynamic_func_defs[i];
56+
if (nested->fn_flags2 & ZEND_ACC2_SCOPE_FUNC) {
57+
scope_fn_visit_parent_cvs(nested, used_vars, vars_map);
58+
}
59+
}
60+
#undef VISIT_CV
61+
}
62+
2163
/* This pass removes all CVs and temporaries that are completely unused. It does *not* merge any CVs or TMPs.
2264
* This pass does not operate on SSA form anymore. */
2365
void zend_optimizer_compact_vars(zend_op_array *op_array) {
2466
int i;
2567

68+
/* Scope fn CVs are handled by the ancestor. Just skip this here. */
69+
if (op_array->fn_flags2 & ZEND_ACC2_SCOPE_FUNC) {
70+
return;
71+
}
72+
2673
ALLOCA_FLAG(use_heap1);
2774
ALLOCA_FLAG(use_heap2);
2875
uint32_t used_vars_len = zend_bitset_len(op_array->last_var + op_array->T);
@@ -52,6 +99,13 @@ void zend_optimizer_compact_vars(zend_op_array *op_array) {
5299
}
53100
}
54101

102+
for (i = 0; i < (int)op_array->num_dynamic_func_defs; i++) {
103+
zend_op_array *child = op_array->dynamic_func_defs[i];
104+
if (child->fn_flags2 & ZEND_ACC2_SCOPE_FUNC) {
105+
scope_fn_visit_parent_cvs(child, used_vars, NULL);
106+
}
107+
}
108+
55109
num_cvs = 0;
56110
for (i = 0; i < op_array->last_var; i++) {
57111
if (zend_bitset_in(used_vars, i)) {
@@ -93,6 +147,13 @@ void zend_optimizer_compact_vars(zend_op_array *op_array) {
93147
}
94148
}
95149

150+
for (i = 0; i < (int)op_array->num_dynamic_func_defs; i++) {
151+
zend_op_array *child = op_array->dynamic_func_defs[i];
152+
if (child->fn_flags2 & ZEND_ACC2_SCOPE_FUNC) {
153+
scope_fn_visit_parent_cvs(child, NULL, vars_map);
154+
}
155+
}
156+
96157
/* Update CV name table */
97158
if (num_cvs != op_array->last_var) {
98159
if (num_cvs) {

Zend/Optimizer/optimize_temp_vars_5.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ void zend_optimize_temporary_variables(zend_op_array *op_array, zend_optimizer_c
122122
}
123123
}
124124
}
125+
/* DECLARE_SCOPE_FUNC op1 is never consumed, just used for itself.
126+
* It has to simply exist for the whole function, so it gets its own TMP. */
127+
if (opline->opcode == ZEND_DECLARE_SCOPE_FUNC) {
128+
use_new_var = 1;
129+
}
125130
if (use_new_var) {
126131
i = ++max;
127132
zend_bitset_incl(taken_T, i);

Zend/Optimizer/zend_call_graph.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,18 @@
2525
static void zend_op_array_calc(zend_op_array *op_array, void *context)
2626
{
2727
zend_call_graph *call_graph = context;
28+
if (op_array->fn_flags2 & ZEND_ACC2_SCOPE_FUNC) {
29+
return;
30+
}
2831
call_graph->op_arrays_count++;
2932
}
3033

3134
static void zend_op_array_collect(zend_op_array *op_array, void *context)
3235
{
3336
zend_call_graph *call_graph = context;
37+
if (op_array->fn_flags2 & ZEND_ACC2_SCOPE_FUNC) {
38+
return;
39+
}
3440
zend_func_info *func_info = call_graph->func_infos + call_graph->op_arrays_count;
3541

3642
ZEND_SET_FUNC_INFO(op_array, func_info);

Zend/Optimizer/zend_inference.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4010,9 +4010,12 @@ static zend_always_inline zend_result _zend_update_type_info(
40104010
UPDATE_SSA_TYPE(MAY_BE_BOOL, ssa_op->result_def);
40114011
break;
40124012
case ZEND_DECLARE_LAMBDA_FUNCTION:
4013+
case ZEND_DECLARE_SCOPE_FUNC:
40134014
UPDATE_SSA_TYPE(MAY_BE_OBJECT|MAY_BE_RC1|MAY_BE_RCN, ssa_op->result_def);
40144015
UPDATE_SSA_OBJ_TYPE(zend_ce_closure, /* is_instanceof */ false, ssa_op->result_def);
40154016
break;
4017+
case ZEND_ENTER_SCOPE_FUNC:
4018+
break;
40164019
case ZEND_PRE_DEC_STATIC_PROP:
40174020
case ZEND_PRE_INC_STATIC_PROP:
40184021
case ZEND_POST_DEC_STATIC_PROP:

Zend/Optimizer/zend_optimizer.c

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,13 +1048,19 @@ zend_op *zend_optimizer_get_loop_var_def(const zend_op_array *op_array, zend_op
10481048
return NULL;
10491049
}
10501050

1051+
static zend_always_inline bool zend_op_array_is_scope_fn(const zend_op_array *op_array) {
1052+
return (op_array->fn_flags2 & ZEND_ACC2_SCOPE_FUNC) != 0;
1053+
}
1054+
10511055
static void zend_optimize(zend_op_array *op_array,
10521056
zend_optimizer_ctx *ctx)
10531057
{
10541058
if (op_array->type == ZEND_EVAL_CODE) {
10551059
return;
10561060
}
10571061

1062+
bool is_scope_fn = zend_op_array_is_scope_fn(op_array);
1063+
10581064
if (ctx->debug_level & ZEND_DUMP_BEFORE_OPTIMIZER) {
10591065
zend_dump_op_array(op_array, ZEND_DUMP_LIVE_RANGES, "before optimizer", NULL);
10601066
}
@@ -1106,9 +1112,13 @@ static void zend_optimize(zend_op_array *op_array,
11061112

11071113
/* pass 6:
11081114
* - DFA optimization
1109-
*/
1115+
*
1116+
* Skipped for scope-fn op_arrays: DCE on a body's CV write would
1117+
* eliminate a store the parent later reads, but single-op_array SSA
1118+
* cannot see that. Re-enabling needs cross-op-array DCE protection
1119+
* (treat every body CV write as live-out). */
11101120
if ((ZEND_OPTIMIZER_PASS_6 & ctx->optimization_level) &&
1111-
!(ZEND_OPTIMIZER_PASS_7 & ctx->optimization_level)) {
1121+
!(ZEND_OPTIMIZER_PASS_7 & ctx->optimization_level) && !is_scope_fn) {
11121122
zend_optimize_dfa(op_array, ctx);
11131123
if (ctx->debug_level & ZEND_DUMP_AFTER_PASS_6) {
11141124
zend_dump_op_array(op_array, 0, "after pass 6", NULL);
@@ -1141,7 +1151,8 @@ static void zend_optimize(zend_op_array *op_array,
11411151
*/
11421152
if ((ZEND_OPTIMIZER_PASS_11 & ctx->optimization_level) &&
11431153
(!(ZEND_OPTIMIZER_PASS_6 & ctx->optimization_level) ||
1144-
!(ZEND_OPTIMIZER_PASS_7 & ctx->optimization_level))) {
1154+
!(ZEND_OPTIMIZER_PASS_7 & ctx->optimization_level) ||
1155+
is_scope_fn /* normally passes 6/7 would handle it, but scope fns are exempt */)) {
11451156
zend_optimizer_compact_literals(op_array, ctx);
11461157
if (ctx->debug_level & ZEND_DUMP_AFTER_PASS_11) {
11471158
zend_dump_op_array(op_array, 0, "after pass 11", NULL);
@@ -1172,6 +1183,8 @@ static void zend_revert_pass_two(zend_op_array *op_array)
11721183

11731184
ZEND_ASSERT((op_array->fn_flags & ZEND_ACC_DONE_PASS_TWO) != 0);
11741185

1186+
zend_pass_two_revert_scope_fn_reservations(op_array);
1187+
11751188
opline = op_array->opcodes;
11761189
const zend_op *end = opline + op_array->last;
11771190
while (opline < end) {
@@ -1304,6 +1317,7 @@ static void zend_redo_pass_two(zend_op_array *op_array)
13041317
}
13051318

13061319
op_array->fn_flags |= ZEND_ACC_DONE_PASS_TWO;
1320+
zend_pass_two_install_scope_fn_reservations(op_array);
13071321
}
13081322

13091323
static void zend_redo_pass_two_ex(zend_op_array *op_array, const zend_ssa *ssa)
@@ -1446,23 +1460,35 @@ static void zend_redo_pass_two_ex(zend_op_array *op_array, const zend_ssa *ssa)
14461460
}
14471461

14481462
op_array->fn_flags |= ZEND_ACC_DONE_PASS_TWO;
1463+
zend_pass_two_install_scope_fn_reservations(op_array);
14491464
}
14501465

14511466
static void zend_optimize_op_array(zend_op_array *op_array,
14521467
zend_optimizer_ctx *ctx)
14531468
{
1469+
bool is_scope_fn = zend_op_array_is_scope_fn(op_array);
1470+
1471+
/* Make scope fns individually optimizeable first, before running optimizer. */
1472+
uint32_t saved_scope_T = is_scope_fn ? op_array->T : 0;
1473+
uint32_t saved_scope_ex_offset = is_scope_fn ? zend_unfixup_scope_func_self(op_array, saved_scope_T) : 0;
1474+
14541475
/* Revert pass_two() */
14551476
zend_revert_pass_two(op_array);
14561477

14571478
/* Do actual optimizations */
14581479
zend_optimize(op_array, ctx);
14591480

1460-
/* Redo pass_two() */
1461-
zend_redo_pass_two(op_array);
1462-
1481+
/* Live ranges must be recalculated before scope fn fixup. */
14631482
if (op_array->live_range) {
14641483
zend_recalc_live_ranges(op_array, NULL);
14651484
}
1485+
1486+
if (is_scope_fn) {
1487+
zend_refixup_scope_func_self(op_array, saved_scope_ex_offset, saved_scope_T);
1488+
}
1489+
1490+
/* Redo pass_two() */
1491+
zend_redo_pass_two(op_array);
14661492
}
14671493

14681494
static void zend_adjust_fcall_stack_size(const zend_op_array *op_array, const zend_optimizer_ctx *ctx)
@@ -1576,6 +1602,12 @@ static void step_optimize_op_array(zend_op_array *op_array, void *context) {
15761602
zend_optimize_op_array(op_array, (zend_optimizer_ctx *) context);
15771603
}
15781604

1605+
static void step_optimize_scope_fn_op_array(zend_op_array *op_array, void *context) {
1606+
if (zend_op_array_is_scope_fn(op_array)) {
1607+
zend_optimize_op_array(op_array, (zend_optimizer_ctx *) context);
1608+
}
1609+
}
1610+
15791611
static void step_adjust_fcall_stack_size(zend_op_array *op_array, void *context) {
15801612
zend_adjust_fcall_stack_size(op_array, (zend_optimizer_ctx *) context);
15811613
}
@@ -1634,6 +1666,12 @@ ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_l
16341666
}
16351667

16361668
for (i = 0; i < call_graph.op_arrays_count; i++) {
1669+
/* Single-op_array SSA can't see scope-fn children's reads of
1670+
* parent CVs. Subsequent DFA-based passes (DCE, SCCP) would
1671+
* treat parent CVs only used by a child as dead. */
1672+
if (call_graph.op_arrays[i]->fn_flags2 & ZEND_ACC2_HAS_TRACKED_TEMPORARIES) {
1673+
continue;
1674+
}
16371675
func_info = ZEND_FUNC_INFO(call_graph.op_arrays[i]);
16381676
if (func_info) {
16391677
if (zend_dfa_analyze_op_array(call_graph.op_arrays[i], &ctx, &func_info->ssa) == SUCCESS) {
@@ -1646,6 +1684,9 @@ ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_l
16461684

16471685
//TODO: perform inner-script inference???
16481686
for (i = 0; i < call_graph.op_arrays_count; i++) {
1687+
if (call_graph.op_arrays[i]->fn_flags2 & ZEND_ACC2_HAS_TRACKED_TEMPORARIES) {
1688+
continue;
1689+
}
16491690
func_info = ZEND_FUNC_INFO(call_graph.op_arrays[i]);
16501691
if (func_info) {
16511692
zend_dfa_optimize_op_array(call_graph.op_arrays[i], &ctx, &func_info->ssa, func_info->call_map);
@@ -1685,14 +1726,9 @@ ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_l
16851726
}
16861727
}
16871728

1688-
if (ZEND_OPTIMIZER_PASS_12 & optimization_level) {
1689-
for (i = 0; i < call_graph.op_arrays_count; i++) {
1690-
zend_adjust_fcall_stack_size_graph(call_graph.op_arrays[i]);
1691-
}
1692-
}
1693-
16941729
for (i = 0; i < call_graph.op_arrays_count; i++) {
16951730
op_array = call_graph.op_arrays[i];
1731+
zend_op *old_opcodes = op_array->opcodes;
16961732
func_info = ZEND_FUNC_INFO(op_array);
16971733
if (func_info && func_info->ssa.var_info) {
16981734
zend_redo_pass_two_ex(op_array, &func_info->ssa);
@@ -1705,6 +1741,31 @@ ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_l
17051741
zend_recalc_live_ranges(op_array, NULL);
17061742
}
17071743
}
1744+
/* Update offsets for pass 12, as zend_redo_pass_two may reallocate */
1745+
if (func_info && op_array->opcodes != old_opcodes) {
1746+
ptrdiff_t delta = op_array->opcodes - old_opcodes;
1747+
zend_call_info *call = func_info->callee_info;
1748+
while (call) {
1749+
if (call->caller_init_opline) {
1750+
call->caller_init_opline += delta;
1751+
}
1752+
if (call->caller_call_opline) {
1753+
call->caller_call_opline += delta;
1754+
}
1755+
call = call->next_callee;
1756+
}
1757+
}
1758+
}
1759+
1760+
/* zend_build_call_graph ignores scope fns, so explicitely optimize them here. */
1761+
zend_foreach_op_array(script, step_optimize_scope_fn_op_array, &ctx);
1762+
1763+
/* PASS_12 requires knowledge of the final callee->T to compute INIT_FCALL stack sizes;
1764+
* for scope fns these change during zend_redo_pass_two, hence we do this late. */
1765+
if (ZEND_OPTIMIZER_PASS_12 & optimization_level) {
1766+
for (i = 0; i < call_graph.op_arrays_count; i++) {
1767+
zend_adjust_fcall_stack_size_graph(call_graph.op_arrays[i]);
1768+
}
17081769
}
17091770

17101771
for (i = 0; i < call_graph.op_arrays_count; i++) {

Zend/tests/function_arguments/gh20435_2.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
--TEST--
2-
GH-20435: ZEND_CALL_HAS_EXTRA_NAMED_PARAMS & magic methods in debug_backtrace_get_args()
2+
GH-20435: ZEND_CALL_MAYBE_HAS_EXTRA_NAMED_PARAMS & magic methods in debug_backtrace_get_args()
33
--FILE--
44
<?php
55

0 commit comments

Comments
 (0)