Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c7d1a72
Partial application
arnaud-lb Aug 5, 2025
3ede63b
Generate static closure when possible. Use static:: for static methods.
arnaud-lb Sep 14, 2025
65e9090
Fix tests
arnaud-lb Sep 14, 2025
8ff4e52
Use only function as cache key. Fix inline cache.
arnaud-lb Sep 14, 2025
db52929
Fix jit
arnaud-lb Sep 14, 2025
5cdf339
Restore bindTo() rules (TODO: rebind internal closure)
arnaud-lb Sep 14, 2025
e0f8bde
Reorganize zend_partial_create / zp_compile
arnaud-lb Oct 4, 2025
a2836bc
Fix arg
arnaud-lb Oct 4, 2025
cd76d82
Use arena
arnaud-lb Oct 4, 2025
592b161
Fix pass-by-ref variadic
arnaud-lb Oct 4, 2025
c9c2ae9
Simplify
arnaud-lb Oct 4, 2025
e7c35a8
Remove trailing new line in tests
arnaud-lb Oct 4, 2025
7fdcb6c
Print exception class
arnaud-lb Oct 4, 2025
3f26b83
Update Zend/tests/partial_application/clone.phpt
arnaud-lb Oct 4, 2025
9239a90
Fix tests when opcache is not enabled
arnaud-lb Oct 8, 2025
f4323f8
Fix mem leak when opcache is not enabled
arnaud-lb Oct 8, 2025
0b967b7
Fix UAF when opcache is not enabled
arnaud-lb Oct 8, 2025
af3ee05
Fix test
arnaud-lb Oct 9, 2025
6c5bdf7
Variadic placeholder should be last (including after named args)
arnaud-lb Oct 9, 2025
574c566
Forward superfluous args
arnaud-lb Oct 20, 2025
817e7a3
Adjust error messages
arnaud-lb Oct 20, 2025
4f3a594
Add param reordering
arnaud-lb Oct 10, 2025
d77cd6d
Adjust tests
arnaud-lb Nov 3, 2025
1fcf3ef
zend_ast_create_fcc() signature
arnaud-lb Nov 3, 2025
b753b0c
Review
arnaud-lb Nov 3, 2025
616a914
Support return by ref
arnaud-lb Nov 17, 2025
f6fec4e
Review
arnaud-lb Nov 18, 2025
2bfdb8b
Init result
arnaud-lb Nov 18, 2025
809e2c7
Fix build
arnaud-lb Nov 18, 2025
bbba27e
Fix tests
arnaud-lb Nov 19, 2025
c1345e8
Improve PFA func name
arnaud-lb Nov 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Zend/Optimizer/compact_literals.c
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,7 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
case ZEND_SEND_VAR_NO_REF_EX:
case ZEND_SEND_REF:
case ZEND_SEND_FUNC_ARG:
case ZEND_SEND_PLACEHOLDER:
case ZEND_CHECK_FUNC_ARG:
if (opline->op2_type == IS_CONST) {
opline->result.num = cache_size;
Expand All @@ -747,6 +748,10 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
cache_size += sizeof(void *);
}
break;
case ZEND_CALLABLE_CONVERT_PARTIAL:
opline->op1.num = cache_size;
cache_size += 2 * sizeof(void *);
break;
}
opline++;
}
Expand Down
6 changes: 4 additions & 2 deletions Zend/Optimizer/optimize_func_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx)
case ZEND_DO_UCALL:
case ZEND_DO_FCALL_BY_NAME:
case ZEND_CALLABLE_CONVERT:
case ZEND_CALLABLE_CONVERT_PARTIAL:
call--;
if (call_stack[call].func && call_stack[call].opline) {
zend_op *fcall = call_stack[call].opline;
Expand Down Expand Up @@ -225,13 +226,14 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx)
* At this point we also know whether or not the result of
* the DO opcode is used, allowing to optimize calls to
* ZEND_ACC_NODISCARD functions. */
if (opline->opcode != ZEND_CALLABLE_CONVERT) {
if (opline->opcode != ZEND_CALLABLE_CONVERT && opline->opcode != ZEND_CALLABLE_CONVERT_PARTIAL) {
opline->opcode = zend_get_call_op(fcall, call_stack[call].func, !RESULT_UNUSED(opline));
}

if ((ZEND_OPTIMIZER_PASS_16 & ctx->optimization_level)
&& call_stack[call].try_inline
&& opline->opcode != ZEND_CALLABLE_CONVERT) {
&& opline->opcode != ZEND_CALLABLE_CONVERT
&& opline->opcode != ZEND_CALLABLE_CONVERT_PARTIAL) {
zend_try_inline_call(op_array, fcall, opline, call_stack[call].func);
}
}
Expand Down
5 changes: 5 additions & 0 deletions Zend/Optimizer/zend_call_graph.c
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ ZEND_API void zend_analyze_calls(zend_arena **arena, zend_script *script, uint32
case ZEND_DO_UCALL:
case ZEND_DO_FCALL_BY_NAME:
case ZEND_CALLABLE_CONVERT:
case ZEND_CALLABLE_CONVERT_PARTIAL:
func_info->flags |= ZEND_FUNC_HAS_CALLS;
if (call_info) {
call_info->caller_call_opline = opline;
Expand All @@ -144,11 +145,15 @@ ZEND_API void zend_analyze_calls(zend_arena **arena, zend_script *script, uint32
case ZEND_SEND_VAR_NO_REF:
case ZEND_SEND_VAR_NO_REF_EX:
case ZEND_SEND_USER:
case ZEND_SEND_PLACEHOLDER:
if (call_info) {
if (opline->op2_type == IS_CONST) {
call_info->named_args = true;
break;
}
if (opline->opcode == ZEND_SEND_PLACEHOLDER && opline->op1.num == _IS_PLACEHOLDER_VARIADIC) {
break;
}

uint32_t num = opline->op2.num;
if (num > 0) {
Expand Down
1 change: 1 addition & 0 deletions Zend/Optimizer/zend_inference.c
Original file line number Diff line number Diff line change
Expand Up @@ -3906,6 +3906,7 @@ static zend_always_inline zend_result _zend_update_type_info(
}
break;
case ZEND_CALLABLE_CONVERT:
case ZEND_CALLABLE_CONVERT_PARTIAL:
UPDATE_SSA_TYPE(MAY_BE_OBJECT | MAY_BE_RC1 | MAY_BE_RCN, ssa_op->result_def);
UPDATE_SSA_OBJ_TYPE(zend_ce_closure, /* is_instanceof */ false, ssa_op->result_def);
break;
Expand Down
92 changes: 92 additions & 0 deletions Zend/tests/partial_application/attributes_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
--TEST--
Closure application copies attributes
--XFAIL--
Only NoDiscard and SensitiveParameter are copied
--FILE--
<?php

#[Attribute]
class Test {}

#[NoDiscard]
function f($a, #[SensitiveParameter] $b, #[Test] ...$c) {
}

function dump_attributes($function) {
echo "Function attributes:\n";
$r = new ReflectionFunction($function);
var_dump($r->getAttributes());

foreach ($r->getParameters() as $i => $p) {
echo "Parameter $i:\n";
var_dump($p->getAttributes());
}
}

dump_attributes('f');

$f = f(1, ?, ?, ...);

dump_attributes($f);

?>
--EXPECTF--
Function attributes:
array(1) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "NoDiscard"
}
}
Parameter 0:
array(0) {
}
Parameter 1:
array(1) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(18) "SensitiveParameter"
}
}
Parameter 2:
array(1) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(4) "Test"
}
}
Function attributes:
array(1) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "NoDiscard"
}
}
Parameter 0:
array(1) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(18) "SensitiveParameter"
}
}
Parameter 1:
array(1) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(4) "Test"
}
}
Parameter 2:
array(1) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(4) "Test"
}
}
53 changes: 53 additions & 0 deletions Zend/tests/partial_application/attributes_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
--TEST--
Closure application preserves #[SensitiveParameter]
--FILE--
<?php

function f($a, #[SensitiveParameter] $b, $c) {
throw new Exception();
}

echo "# During partial application:\n";

try {
$f = f(1, 'sensitive');
} catch (Error $e) {
echo $e, "\n\n";
}

echo "# In trampoline:\n";

try {
$f = f(1, ?, ?)('sensitive');
} catch (Error $e) {
echo $e, "\n\n";
}

echo "# In execution:\n";

try {
$f = f(1, ?, ?)('sensitive', 3);
} catch (Exception $e) {
echo $e, "\n";
}

?>
--EXPECTF--
# During partial application:
ArgumentCountError: Too few arguments to function f(), 2 passed in %s on line %d and exactly 3 expected in %s:%d
Stack trace:
#0 %s(%d): f(1, Object(SensitiveParameterValue))
#1 {main}

# In trampoline:
ArgumentCountError: Too few arguments to function {closure:%s:%d}(), 1 passed in %s on line %d and exactly 2 expected in %s:%d
Stack trace:
#0 %s(%d): {closure:%s}(Object(SensitiveParameterValue))
#1 {main}

# In execution:
Exception in %s:%d
Stack trace:
#0 %s(%d): f(1, Object(SensitiveParameterValue), 3)
#1 %s(%d): {closure:%s}(Object(SensitiveParameterValue), 3)
#2 {main}
16 changes: 16 additions & 0 deletions Zend/tests/partial_application/attributes_003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
--TEST--
Closure application preserves #[NoDiscard]
--FILE--
<?php

#[NoDiscard] function f($a) {
}

$f = f(?);
$f(1);

(void) $f(1);

?>
--EXPECTF--
Warning: The return value of function {closure:%s}() should either be used or intentionally ignored by casting it as (void) in %s on line 7
50 changes: 50 additions & 0 deletions Zend/tests/partial_application/clone.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
--TEST--
Closure application clone
--FILE--
<?php

class C {
public function __construct(
public mixed $a,
public mixed $b,
) { }
}

$clone = clone(?);
var_dump($clone(new C(1, 2)));

$clone = clone(...);
var_dump($clone(new C(3, 4)));

$clone = clone(new C(5, 6), ?);
var_dump($clone(['a' => 7]));

$clone = clone(?, ['a' => 8]);
var_dump($clone(new C(9, 10)));

?>
--EXPECTF--
object(C)#%d (2) {
["a"]=>
int(1)
["b"]=>
int(2)
}
object(C)#%d (2) {
["a"]=>
int(3)
["b"]=>
int(4)
}
object(C)#%d (2) {
["a"]=>
int(7)
["b"]=>
int(6)
}
object(C)#%d (2) {
["a"]=>
int(8)
["b"]=>
int(10)
}
8 changes: 8 additions & 0 deletions Zend/tests/partial_application/compile_errors_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--TEST--
Closure application compile errors: multiple ...
--FILE--
<?php
foo(..., ...);
?>
--EXPECTF--
Fatal error: Variadic placeholder may only appear once in %s on line %d
8 changes: 8 additions & 0 deletions Zend/tests/partial_application/compile_errors_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--TEST--
Closure application compile errors: ... must be last
--FILE--
<?php
foo(..., ?);
?>
--EXPECTF--
Fatal error: Variadic placeholder must be last in %s on line %d
8 changes: 8 additions & 0 deletions Zend/tests/partial_application/compile_errors_003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--TEST--
Closure application compile errors: named arguments must come after placeholder
--FILE--
<?php
foo(n: 5, ?);
?>
--EXPECTF--
Fatal error: Cannot use positional argument after named argument in %s on line %d
8 changes: 8 additions & 0 deletions Zend/tests/partial_application/compile_errors_004.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--TEST--
Closure application compile errors: named arguments must come before variadic placeholder
--FILE--
<?php
foo(..., n: 5);
?>
--EXPECTF--
Fatal error: Variadic placeholder must be last in %s on line %d
8 changes: 8 additions & 0 deletions Zend/tests/partial_application/compile_errors_005.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--TEST--
Closure application compile errors: follow variadic with un-named arg
--FILE--
<?php
foo(..., $a);
?>
--EXPECTF--
Fatal error: Variadic placeholder must be last in %s on line %d
8 changes: 8 additions & 0 deletions Zend/tests/partial_application/compile_errors_006.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--TEST--
Closure application compile errors: mix application with unpack (placeholder after)
--FILE--
<?php
foo(...["foo" => "bar"], ...);
?>
--EXPECTF--
Fatal error: Cannot combine partial application and unpacking in %s on line %d
8 changes: 8 additions & 0 deletions Zend/tests/partial_application/compile_errors_007.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--TEST--
Closure application compile errors: mix application with unpack
--FILE--
<?php
foo(...["foo" => "bar"], ...);
?>
--EXPECTF--
Fatal error: Cannot combine partial application and unpacking %s on line %d
Loading
Loading