feat: support static by-reference variadics

master^2
韩天峰 3 hours ago
parent f2db98b7da
commit d11637faf0
  1. 10
      docs/en/INCOMPATIBLE_PHP_FEATURES.md
  2. 6
      docs/en/PHP_INCOMPATIBILITY_CLASSIFICATION.md
  3. 4
      docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md
  4. 4
      phpunit/src/FunctionTest.php
  5. 73
      phpunit/src/NegativeCompatibilityTest.php
  6. 29
      src/Generator/CallArgumentGenerator.php
  7. 59
      src/Generator/ClosureGenerator.php
  8. 9
      src/Generator/TypeCheckGenerator.php
  9. 10
      src/Parser/TypeConversionTrait.php
  10. 10
      src/Preprocessor.php
  11. 4
      src/Translator.php
  12. 9
      tests/compiler/SKIP_TESTS.md
  13. 45
      tests/compiler/closure/by-reference-parameters.phpt
  14. 25
      tests/compiler/ref/ref-closure-param.phpt
  15. 111
      tests/compiler/variadic/by-reference-basic.phpt
  16. 49
      tests/compiler/variadic/by-reference-closure-callback.phpt
  17. 50
      tests/compiler/variadic/by-reference-dynamic-explicit.phpt
  18. 39
      tests/compiler/variadic/by-reference-inheritance.phpt
  19. 93
      tests/compiler/variadic/by-reference-types.phpt
  20. 99
      tests/compiler/variadic/by-reference-unpack.phpt

@ -61,7 +61,10 @@ incompatible with or more restrictive than standard PHP.
- `__construct()` may not have a return value.
- A parameter with a default value may not appear before a required parameter
(PHP permits this legacy pattern but treats the former parameter as required).
- Variadic parameters by reference `&...$args` are not supported.
- By-reference variadic parameters `&...$args` are supported for ordinary
functions and methods whose signature is known at compile time, including
direct, named, and unpacked arguments. A by-reference variadic declaration on
a dynamic Closure is not supported.
- Union, intersection, and nullable types are still represented as `mixed/any`
in C++, but the static analysis phase uses known expression types to reject
definitely incompatible arguments, return values, and property assignments
@ -88,7 +91,10 @@ incompatible with or more restrictive than standard PHP.
functions, ordinary methods, and native direct calls with known signatures;
do not mistakenly describe the compiler's internal cross-trait dynamic-dispatch
limitation as "TypePHP does not support reference parameters".
- Closures and arrow functions do not support reference parameters.
- Closures and arrow functions support fixed by-reference parameters. Because a
Closure invocation is dynamically dispatched, the caller must still mark
reference arguments explicitly with `refval()` / `toRef()`; Zend callbacks
use the generated Closure arginfo automatically.
- Reference assignment cannot create a reference from a complex static-property
expression.
- Calls whose argument signature cannot be determined at compile time — dynamic

@ -77,6 +77,8 @@ These items should be documented with the exact boundary.
| Reserved keyword methods such as `toArray()` | Intentional Rule | Conversion keywords are resolved before ordinary object methods to keep conversion lowering static and predictable. |
| Zero-initialized fixed typed property slots | Intentional Rule / Partial | Native fixed-layout slots use their type's zero value instead of preserving every Zend uninitialized-property transition. |
| Structural mutation of `std` containers during `foreach` | Intentional Rule | Native C++ iterators may be invalidated by append, insertion, erase or whole-container replacement. TypePHP rejects these operations inside the active loop while allowing non-structural element updates. |
| Automatic reference inference for dynamic calls | Intentional Rule | A runtime callable may resolve to a function, method, or Closure unknown to the compiler. TypePHP does not mirror callable signatures at runtime; callers must use `refval()` / `toRef()` explicitly. |
| By-reference variadic parameters on dynamic Closures | Intentional Rule | Supporting `&...` here would require signature-aware runtime argument packing. Statically resolved ordinary functions and methods support `&...`; dynamic Closures do not. |
## Implementable but Currently Unsupported
@ -86,11 +88,7 @@ These items should be documented with the exact boundary.
| Variable variables (`$$var`) | Pending | Add a function-local symbol table mirror for dynamic locals, and disable or synchronize native locals that escape into dynamic lookup. |
| Closure or arrow function returning by reference | Pending | Closure metadata and wrappers must preserve return-by-reference and emit `ReturnRef`. |
| PHP 8.5 closures in constants, parameter defaults or property defaults | Pending | Use context-aware runtime initializers: cache constants and property defaults per request, create parameter defaults per omitted call, and never place request-local zvals in persistent MINIT storage. |
| Closure and arrow function by-reference parameters | Pending | Closure arginfo must preserve by-reference parameters and call lowering must pass reference slots. |
| By-reference variadic parameters (`&...$args`) | Pending | Variadic storage must preserve references instead of copying values. |
| By-reference parameters with default values | Pending | Need PHP-compatible handling for omitted arguments using temporary default values while still binding references for passed arguments. |
| Reference assignment from complex static property expressions | Pending | Static property reference targets need complete lowering and lifetime handling. |
| Dynamic calls automatically converting by-reference arguments | Pending | Runtime callable metadata or reflection can identify by-reference parameters and build reference arguments dynamically. |
| Calls with unpack plus trailing named arguments staying native | Pending | Normalize and reorder call arguments in IR before native-call selection. |
| Dynamic `parent::method()` name | Pending | Needs runtime parent method lookup with correct call scope. |
| Private typed property access on cloned objects through variables | Pending / Partial | Requires a complete declaring-class-aware access resolver. |

@ -28,7 +28,7 @@
- 暂不支持 PHP 8.5 在全局常量、类常量、参数默认值或属性默认值中使用 `static function`;初始化表达式内嵌套的闭包同样会在编译期被拒绝。
- `__construct()` 不允许返回值。
- 参数默认值不允许出现在必填参数之前(`PHP`允许,但会直接丢弃此默认参数)。
- 支持引用可变参数 `&...$args`
- 已知编译期签名的普通函数和方法支持引用可变参数 `&...$args`,包括直接参数、命名参数和参数展开;动态 Closure 暂不支持声明引用可变参数
- 联合类型、交叉类型、`nullable` 类型仍以 `mixed/any` 作为 C++ 表示,但静态阶段会利用已知表达式类型提前拒绝确定不兼容的参数、返回值和属性赋值;动态值仍保留运行时 type check。
- 局部变量类型一旦被静态推断为具体 native 类型,不支持在同一作用域内重新赋值为不兼容类型。
@ -44,7 +44,7 @@
- `exit(message: $value)` 可作为 TypePHP named-argument 扩展使用;它与位置参数 `exit($value)` 进入同一退出路径。
- TypePHP 使用严格参数数量规则:非 variadic 函数不接受声明范围之外的额外参数;`func_get_args()` 不会隐式放宽签名。
- 已知签名的普通函数、普通方法和 native 直调支持引用参数及写回;不要把编译器内部跨 Trait 动态分派的限制误写成“TypePHP 不支持引用参数”。
- 闭包和箭头函数支持引用参数。
- 闭包和箭头函数支持固定引用参数。Closure 调用属于动态分派,调用方仍须通过 `refval()` / `toRef()` 显式标记引用参数;由 Zend 发起 callback 时则会自动使用编译器生成的 Closure arginfo
- 引用赋值不支持从复杂静态属性表达式建立引用。
- 动态调用、闭包调用等编译期无法确定参数签名的调用,不能自动转换引用参数;需要显式使用 `refval()` 或等价关键词方法 `toRef()`
- `refval()` / `toRef()` 只接受变量、数组元素或对象属性。

@ -120,12 +120,12 @@ class FunctionTest extends \BaseTest
public function testClosureReferenceParameter()
{
$this->exec('Closure cannot use reference parameter', 'closure-ref-param.php');
$this->compile('closure-ref-param.php');
}
public function testVariadicReferenceParameter()
{
$this->exec('Variadic parameters cannot be passed by reference', 'variadic-ref-param.php');
$this->compile('variadic-ref-param.php');
}
public function testOptionalParameterBeforeRequiredParameter()

@ -193,81 +193,96 @@ function main(): void
PHP,
];
yield 'closure reference parameter' => [
yield 'closure reference return' => [
'convert',
'Closure cannot use reference parameter',
'Closure and arrow functions cannot return by reference',
<<<'PHP'
<?php
function main(): void
{
$callback = static function (&$value): void { // @diagnostic
$callback = static function &(): mixed { // @diagnostic
static $value = 42;
return $value;
};
}
PHP,
];
yield 'arrow function reference parameter' => [
'convert',
'Closure cannot use reference parameter',
yield 'property get hook reference return' => [
'prepare',
'Property get hooks returning by reference are not supported',
<<<'PHP'
<?php
function main(): void
final class ReferencePropertyHook
{
$callback = static fn (&$value): mixed => $value; // @diagnostic
public string $value {
&get => $this->value; // @diagnostic
}
}
PHP,
];
yield 'closure reference return' => [
yield 'arrow function reference return' => [
'convert',
'Closure and arrow functions cannot return by reference',
<<<'PHP'
<?php
function main(): void
{
$callback = static function &(): mixed { // @diagnostic
static $value = 42;
return $value;
};
$value = 42;
$callback = static fn &(): mixed => $value; // @diagnostic
}
PHP,
];
yield 'property get hook reference return' => [
'prepare',
'Property get hooks returning by reference are not supported',
yield 'dynamic Closure reference variadic parameter' => [
'convert',
'By-reference variadic parameters are not supported on dynamic Closures',
<<<'PHP'
<?php
final class ReferencePropertyHook
function main(): void
{
public string $value {
&get => $this->value; // @diagnostic
}
$callback = static function (&...$values): void { // @diagnostic
};
}
PHP,
];
yield 'arrow function reference return' => [
yield 'literal passed to reference variadic parameter' => [
'convert',
'Closure and arrow functions cannot return by reference',
'The left value of assignment operation can only be variable, array item, object property, class static property',
<<<'PHP'
<?php
function collect(&...$values): void
{
}
function main(): void
{
$value = 42;
$callback = static fn &(): mixed => $value; // @diagnostic
collect(42); // @diagnostic
}
PHP,
];
yield 'reference variadic parameter' => [
'prepare',
'Variadic parameters cannot be passed by reference',
yield 'reference variadic override must preserve by-reference contract' => [
'convert',
'Declaration of `BrokenIncrementer::increment()` must be compatible with `IncrementContract::increment()`',
<<<'PHP'
<?php
function collect(&...$values): array // @diagnostic
interface IncrementContract
{
public function increment(int &...$values): void;
}
class BrokenIncrementer implements IncrementContract // @diagnostic
{
public function increment(int ...$values): void
{
}
}
function main(): void
{
return $values;
}
PHP,
];

@ -144,9 +144,12 @@ trait CallArgumentGenerator
continue;
}
// A single unpacked native array is already the ABI value. Reuse
// it directly instead of allocating and merging a temporary array.
if ($variadicArgCount === 1 && $arg->unpack && $this->isVarExpr($arg->value)) {
$argInfo = $functionDef->argInfoList[$variadicArgIndex];
// A single unpacked by-value native array is already the ABI
// value. A by-reference variadic must still separate the source
// and turn every element into a reference before entering the
// callee, matching Zend's argument-unpacking semantics.
if (!$argInfo->byRef && $variadicArgCount === 1 && $arg->unpack && $this->isVarExpr($arg->value)) {
$var = $this->parseIdentifier($arg->value);
if ($this->getVarType($var) === Type::ARRAY) {
$resolvedArgs[$variadicArgIndex] = $var;
@ -155,16 +158,19 @@ trait CallArgumentGenerator
}
$variadicVar ??= $this->addTmpVar(Type::ARRAY);
$argInfo = $functionDef->argInfoList[$variadicArgIndex];
if ($arg->unpack) {
$this->context->beforeStmtLines[] = $variadicVar . '.merge(' . $this->parseArrayArg($arg) . ');';
$method = $argInfo->byRef ? 'mergeReferences' : 'merge';
$this->context->beforeStmtLines[] = $variadicVar . '.' . $method
. '(' . $this->parseArrayArg($arg) . ');';
} elseif ($variadicName !== null) {
$value = $this->getTypeConvertedArg($arg, $argInfo, $callableName, $variadicArgIndex);
$this->context->beforeStmtLines[] = $variadicVar . '.setValue('
$method = $argInfo->byRef ? 'set' : 'setValue';
$this->context->beforeStmtLines[] = $variadicVar . '.' . $method . '('
. $this->getLiteralString($variadicName) . ', ' . $value . ');';
} else {
$value = $this->getTypeConvertedArg($arg, $argInfo, $callableName, $variadicArgIndex);
$this->context->beforeStmtLines[] = $variadicVar . '.appendValue(' . $value . ');';
$method = $argInfo->byRef ? 'append' : 'appendValue';
$this->context->beforeStmtLines[] = $variadicVar . '.' . $method . '(' . $value . ');';
}
}
@ -175,6 +181,15 @@ trait CallArgumentGenerator
}
if ($variadicVar !== null) {
$resolvedArgs[$variadicArgIndex] = $variadicVar;
if ($functionDef->argInfoList[$variadicArgIndex]->byRef) {
// The aggregation array owns the second reference to every
// caller slot. Release it after the full PHP statement and
// also during C++ exception unwinding into a PHP catch block.
$cleanupGuard = $this->genTmpVarName();
$this->context->beforeStmtLines[] = 'php::ArrayCleanupGuard ' . $cleanupGuard
. '{' . $variadicVar . '};';
$this->context->afterStmtLines[] = $cleanupGuard . '.cleanup();';
}
}
ksort($resolvedArgs);
return implode(', ', $resolvedArgs);

@ -37,15 +37,19 @@ trait ClosureGenerator
? $this->getClassEntryPtr($this->getFullClassName())
: 'nullptr';
}
$parameterNames = [];
$parameterDescriptors = [];
foreach ($params as $param) {
$name = is_string($param->var->name)
? $param->var->name
: $this->unescapeVarName($this->parseIdentifier($param->var));
$parameterNames[] = $this->genCharPtr($name, true);
}
return 'php::newClosure(' . $callback . ', ' . $uses . ', ' . $thisArg . ', ' . $scope
. ', { ' . implode(', ', $parameterNames) . ' })';
$parameterDescriptors[] = 'php::ClosureParameter{'
. $this->genCharPtr($name, true) . ', '
. $this->escapeBool($param->byRef) . ', '
. $this->escapeBool($param->variadic) . ', '
. $this->escapeBool(!$param->variadic && $param->default === null) . '}';
}
return 'php::newClosureWithParameters(' . $callback . ', ' . $uses . ', ' . $thisArg . ', ' . $scope
. ', { ' . implode(', ', $parameterDescriptors) . ' })';
}
protected function parseArrowFunction(Expr\ArrowFunction $expr): string
@ -56,9 +60,6 @@ trait ClosureGenerator
$params = [];
foreach ($expr->params as $i => $param) {
if ($param->byRef) {
$this->fatalError($expr, 'Closure cannot use reference parameter');
}
if ($param->var instanceof Variable) {
$params[$param->var->name] = $i;
}
@ -153,6 +154,14 @@ trait ClosureGenerator
} elseif ($expr->byRef) {
$this->fatalError($expr, 'Closure and arrow functions cannot return by reference');
}
foreach ($params as $param) {
if ($param->byRef && $param->variadic) {
$this->fatalError(
$param,
'By-reference variadic parameters are not supported on dynamic Closures',
);
}
}
$tmpVar = $this->genTmpVarName();
$code = $this->getIndent() .
@ -190,9 +199,6 @@ trait ClosureGenerator
$code .= $this->genParameterCountCheck($requiredArgCount, count($params), $hasVariadic);
foreach ($params as $i => $param) {
if ($param->byRef) {
$this->fatalError($expr, 'Closure cannot use reference parameter');
}
$var = $this->parseIdentifier($param->var);
$phpName = is_string($param->var->name) ? $param->var->name : $this->unescapeVarName($var);
if ($param->variadic) {
@ -210,11 +216,20 @@ trait ClosureGenerator
$code .= $this->genClosureParamTypeCheck($param, $var, $phpName, $i, true);
continue;
}
$argExpr = $param->default === null
? 'php::getCallArg(' . $i . ')'
: 'php::getCallArg(' . $i . ', ' . $this->parseParamDefaultValue($param->default) . ')';
$code .= $this->getIndent() . 'auto ' . $var . ' = ' . $argExpr . ';' . PHP_EOL;
$this->addArgument($var, Type::VAR);
if ($param->byRef) {
$argExpr = $param->default === null
? 'php::getCallArgByRef(' . $i . ')'
: 'php::getCallArgByRef(' . $i . ', php::newReference('
. $this->parseParamDefaultValue($param->default) . '))';
$code .= $this->getIndent() . Type::REF . ' ' . $var . ' = ' . $argExpr . ';' . PHP_EOL;
$this->addArgument($var, Type::REF);
} else {
$argExpr = $param->default === null
? 'php::getCallArg(' . $i . ')'
: 'php::getCallArg(' . $i . ', ' . $this->parseParamDefaultValue($param->default) . ')';
$code .= $this->getIndent() . 'auto ' . $var . ' = ' . $argExpr . ';' . PHP_EOL;
$this->addArgument($var, Type::VAR);
}
if (CompileTimeAttribute::consume($param, 'Immutable')) {
$this->context->immutableVars[$var] = true;
if ($this->immutableTypeNodeMayBeObject($param->type)) {
@ -469,11 +484,19 @@ trait ClosureGenerator
private function genClosureParamTypeCheck(Node\Param $param, string $var, string $phpName, int $index, bool $variadic): string
{
if (!$param->type instanceof NullableType && !$param->type instanceof UnionType && !$param->type instanceof IntersectionType) {
if (!$param->byRef
&& !$param->type instanceof NullableType
&& !$param->type instanceof UnionType
&& !$param->type instanceof IntersectionType
) {
return '';
}
if ($param->type === null) {
return '';
}
$typeInfo = $this->buildTypeCheckFromNode($param->type);
$typeInfo = $this->buildTypeCheckFromNode($param->type, $param->byRef);
if (empty($typeInfo['check'])) {
return '';
}

@ -111,7 +111,7 @@ trait TypeCheckGenerator
return $code;
}
protected function buildTypeCheckFromNode(NodeAbstract $typeNode): array
protected function buildTypeCheckFromNode(NodeAbstract $typeNode, bool $includeSimpleType = false): array
{
$check = [];
$typeStr = $this->typeCheckNodeToString($typeNode);
@ -141,8 +141,11 @@ trait TypeCheckGenerator
if (!empty($clause)) {
$check[] = count($clause) === 1 ? $clause[0] : ['kind' => 'allOf', 'types' => $clause];
}
} else {
return ['check' => [], 'typeStr' => ''];
} elseif ($includeSimpleType) {
$clause = $this->buildTypeCheckClause($typeNode);
if (!empty($clause)) {
$check[] = count($clause) === 1 ? $clause[0] : ['kind' => 'allOf', 'types' => $clause];
}
}
if (empty($check)) {

@ -269,6 +269,16 @@ trait TypeConversionTrait
if ($expr instanceof Node\Expr\ArrayDimFetch) {
return $this->parseArrayDimFetchUpdate($expr) . '.toReference()';
}
if ($expr instanceof Node\Expr\PropertyFetch) {
// A normal property read may return a temporary zval. Turning that
// temporary into a reference loses the typed-property source and
// can later detach the wrong source during destruction. Bind the
// reference to the actual property slot instead.
return $this->emitDynamicPropertyFetchRef($expr, $expr);
}
if ($expr instanceof Node\Expr\StaticPropertyFetch) {
return $this->emitStaticPropertyFetchRef($expr, $expr);
}
$var = $this->parseIdentifier($expr);
if ($this->isVarExpr($expr) and $this->isNativeTypeVar($var)) {
$this->context->localVars[$var] = Type::VAR;

@ -861,8 +861,6 @@ class Preprocessor extends CompilerBase
if ($param->variadic) {
if ($i !== $last) {
$this->fatalError($param, 'Variadic parameters must be the last parameter');
} elseif ($param->byRef) {
$this->fatalError($param, 'Variadic parameters cannot be passed by reference');
}
}
if ($param->default && $i < $lastRequiredIndex) {
@ -888,8 +886,12 @@ class Preprocessor extends CompilerBase
if ($param->type === null || $param->type instanceof NullableType) {
$argInfo->nullable = true;
}
if ($param->type instanceof NullableType || $param->type instanceof UnionType || $param->type instanceof IntersectionType) {
$typeInfo = $this->buildTypeCheckFromNode($param->type);
if (($param->byRef && $param->type !== null)
|| $param->type instanceof NullableType
|| $param->type instanceof UnionType
|| $param->type instanceof IntersectionType
) {
$typeInfo = $this->buildTypeCheckFromNode($param->type, $param->byRef);
if (!empty($typeInfo['check']) && !$this->isNativeObjectClass($argInfo->declaredClass)) {
$argInfo->typeCheck = $typeInfo['check'];
$argInfo->typeStr = $typeInfo['typeStr'];

@ -3722,7 +3722,9 @@ CODE;
$cppCode .= $this->getIndent() . Type::ARRAY . ' ' . $var . ';' . PHP_EOL;
$cppCode .= $this->getIndent() . 'for (uint32_t i = ' . $k . '; i < php::getCallArgNum(); i++) {' . PHP_EOL;
$this->indentLevel++;
if ($this->isStrictScalarType($argInfo->type)) {
if ($argInfo->byRef) {
$cppCode .= $this->getIndent() . $var . '.append(php::getCallArgByRef(i));' . PHP_EOL;
} elseif ($this->isStrictScalarType($argInfo->type)) {
$rawVar = 'raw_' . $var;
$cppCode .= $this->getIndent() . Type::VAR . ' ' . $rawVar . ' = php::getCallArg(i);' . PHP_EOL;
$cppCode .= $this->genStrictScalarParamCheck($argInfo, $rawVar, $displayName, 'i + 1');

@ -31,17 +31,12 @@
- **Skip 信息**: `skip: not supported`
- **详细说明**: 复杂的动态属性访问链不支持
### 5. ref-closure-param.phpt
- **原因**: 引用参数闭包不支持
- **Skip 信息**: `skip`
- **详细说明**: 闭包函数中使用引用参数的场景不支持
### 6. innerHTML 相关测试
### 5. innerHTML 相关测试
- **原因**: innerHTML DOM 操作不支持
- **Skip 信息**: `skip innerHTML and DOM manipulation not supported in AOT`
- **详细说明**: JavaScript 风格的 DOM 操作不是 PHP 原生功能
### 7. 游离代码测试
### 6. 游离代码测试
- **原因**: 全局可执行表达式不支持
- **Skip 信息**: `skip Free-floating code not allowed, must be in function/method`
- **详细说明**: 所有可执行表达式必须在函数或类的方法中

@ -0,0 +1,45 @@
--TEST--
Dynamic Closures accept positional arguments explicitly marked with refval
--FILE--
<?php
function main(): void
{
$fixed = static function (&$value): void {
$value .= '!';
};
$text = 'fixed';
$fixed(refval($text));
var_dump($text);
$optional = static function (&$value = null): void {
var_dump($value);
$value = 'private-default';
};
$optional();
$arrow = static fn (&$value): int => ++$value;
$number = 40;
var_dump($arrow(refval($number)), $number);
$typed = static function (int &$value): void {
$value++;
};
$typed(refval($number));
var_dump($number);
$invalid = any('not-an-int');
try {
$typed(refval($invalid));
} catch (TypeError $error) {
echo "typed reference rejected\n";
}
}
?>
--EXPECT--
string(6) "fixed!"
NULL
int(41)
int(41)
int(42)
typed reference rejected

@ -1,21 +1,10 @@
--TEST--
closure function with ref parameter
--SKIPIF--
<?php die('skip'); ?>
--FILE--
<?php
//function main()
//{
// $s = "foo";
// $testFn = function (&$data) {
// $data .= " bar";
// };
// $testFn($s);
// var_dump($s);
//}
function main()
{
$testFn = function (&$data) {
$testFn = function (&$data, $key) {
$data .= " (_)";
};
$sweet = array('a' => 'apple', 'b' => 'banana');
@ -26,4 +15,14 @@ function main()
}
?>
--EXPECT--
string(7) "foo bar"
array(2) {
["sweet"]=>
array(2) {
["a"]=>
string(9) "apple (_)"
["b"]=>
string(10) "banana (_)"
}
["sour"]=>
string(9) "lemon (_)"
}

@ -0,0 +1,111 @@
--TEST--
By-reference variadic parameters preserve direct, named and method arguments
--FILE--
<?php
function suffix(string $suffix, &...$values): array
{
foreach ($values as &$value) {
$value .= $suffix;
}
unset($value);
return array_keys($values);
}
class VariadicReferenceMutator
{
public static function increment(&...$values): void
{
foreach ($values as &$value) {
$value++;
}
unset($value);
}
public function double(&...$values): void
{
foreach ($values as &$value) {
$value *= 2;
}
unset($value);
}
}
class VariadicReferenceTarget
{
public int $value = 10;
public static int $staticValue = 20;
}
function main(): void
{
var_dump(suffix('!'));
$first = 'first';
$second = 'second';
var_dump(suffix('!', $first, $second));
var_dump($first, $second);
$left = 'left';
$right = 'right';
var_dump(suffix(suffix: '?', left: $left, right: $right));
var_dump($left, $right);
$one = 1;
$two = 2;
VariadicReferenceMutator::increment($one, $two);
var_dump($one, $two);
$mutator = new VariadicReferenceMutator();
$mutator->double($one, $two);
var_dump($one, $two);
$array = [7];
$target = new VariadicReferenceTarget();
VariadicReferenceMutator::increment(
$array[0],
$target->value,
VariadicReferenceTarget::$staticValue,
);
var_dump($array, $target->value, VariadicReferenceTarget::$staticValue);
// As in PHP, passing an undefined variable by reference creates it.
VariadicReferenceMutator::increment($createdByReference);
var_dump($createdByReference);
$parameter = (new ReflectionFunction('suffix'))->getParameters()[1];
var_dump($parameter->isVariadic(), $parameter->isPassedByReference());
}
?>
--EXPECT--
array(0) {
}
array(2) {
[0]=>
int(0)
[1]=>
int(1)
}
string(6) "first!"
string(7) "second!"
array(2) {
[0]=>
string(4) "left"
[1]=>
string(5) "right"
}
string(5) "left?"
string(6) "right?"
int(2)
int(3)
int(4)
int(6)
array(1) {
[0]=>
int(8)
}
int(11)
int(21)
int(1)
bool(true)
bool(true)

@ -0,0 +1,49 @@
--TEST--
Reference Closure parameters work at Zend callback boundaries used by Symfony mbstring polyfill
--FILE--
<?php
function convert_values(string $suffix, &...$vars): bool
{
$ok = true;
array_walk_recursive($vars, static function (&$value, $key) use (&$ok, $suffix): void {
if (!is_string($value)) {
$ok = false;
return;
}
$value .= $suffix . ':' . $key;
});
return $ok;
}
function main(): void
{
$first = ['a' => 'one', 'nested' => ['b' => 'two']];
$second = 'three';
var_dump(convert_values('!', $first, $second));
var_dump($first, $second);
$invalid = ['ok', 42];
var_dump(convert_values('?', $invalid));
var_dump($invalid);
}
?>
--EXPECT--
bool(true)
array(2) {
["a"]=>
string(6) "one!:a"
["nested"]=>
array(1) {
["b"]=>
string(6) "two!:b"
}
}
string(8) "three!:1"
bool(false)
array(2) {
[0]=>
string(5) "ok?:0"
[1]=>
int(42)
}

@ -0,0 +1,50 @@
--TEST--
Dynamic calls require explicit refval for by-reference arguments
--FILE--
<?php
function dynamic_increment(&...$values): void
{
foreach ($values as &$value) {
$value++;
}
unset($value);
}
class DynamicReferenceMutator
{
public function suffix(string $suffix, &...$values): void
{
foreach ($values as &$value) {
$value .= $suffix;
}
unset($value);
}
}
function main(): void
{
$function = 'dynamic_increment';
$number = 40;
$function(refval($number));
var_dump($number);
$mutator = new DynamicReferenceMutator();
$method = [$mutator, 'suffix'];
$first = 'one';
$second = 'two';
$method('!', refval($first), refval($second));
var_dump($first, $second);
$closure = static function (&$value): void {
$value .= '?';
};
$closure(refval($second));
var_dump($second);
}
?>
--EXPECT--
int(41)
string(4) "one!"
string(4) "two!"
string(5) "two!?"

@ -0,0 +1,39 @@
--TEST--
By-reference variadic signatures remain compatible across interfaces and inheritance
--FILE--
<?php
declare(strict_types=1);
interface IncrementContract
{
public function increment(int &...$values): void;
}
abstract class IncrementBase implements IncrementContract
{
abstract public function increment(int &...$values): void;
}
final class Incrementer extends IncrementBase
{
public function increment(int &...$values): void
{
foreach ($values as &$value) {
$value++;
}
unset($value);
}
}
function main(): void
{
$incrementer = new Incrementer();
$first = 10;
$second = 20;
$incrementer->increment($first, $second);
var_dump($first, $second);
}
?>
--EXPECT--
int(11)
int(21)

@ -0,0 +1,93 @@
--TEST--
Typed by-reference variadics validate, widen float arguments and write through unions and objects
--FILE--
<?php
declare(strict_types=1);
class Counter
{
public function __construct(public int $value)
{
}
}
function scale(float &...$values): void
{
foreach ($values as &$value) {
$value *= 1.5;
}
unset($value);
}
function normalize(int|string &...$values): void
{
foreach ($values as &$value) {
$value = is_int($value) ? $value + 1 : strtoupper($value);
}
unset($value);
}
function bump_objects(Counter &...$values): void
{
foreach ($values as $value) {
$value->value++;
}
}
function require_ints(int &...$values): void
{
}
function main(): void
{
$integer = 2;
$float = 2.5;
scale($integer, $float);
var_dump($integer, $float);
$values = [4, 6.0];
scale(...$values);
var_dump($values);
$number = 10;
$text = 'hello';
normalize($number, $text);
var_dump($number, $text);
$first = new Counter(1);
$second = new Counter(5);
bump_objects($first, $second);
var_dump($first->value, $second->value);
$invalid = any('not-an-int');
try {
require_ints($invalid);
} catch (TypeError $error) {
echo get_class($error), ': ', $error->getMessage(), PHP_EOL;
}
$invalidUnpack = ['still-not-an-int'];
try {
require_ints(...$invalidUnpack);
} catch (TypeError $error) {
echo "unpack rejected\n";
}
var_dump(ReflectionReference::fromArrayElement($invalidUnpack, 0));
}
?>
--EXPECTF--
float(3)
float(3.75)
array(2) {
[0]=>
float(6)
[1]=>
float(9)
}
int(11)
string(5) "HELLO"
int(2)
int(6)
TypeError: require_ints(): Argument #1 ($values) must be of type int, string given
unpack rejected
NULL

@ -0,0 +1,99 @@
--TEST--
By-reference variadic unpack preserves writeback, COW separation, keys and existing references
--FILE--
<?php
function increment_all(&...$values): array
{
foreach ($values as &$value) {
$value++;
}
unset($value);
return array_keys($values);
}
function main(): void
{
$source = [1, 2];
$copy = $source;
var_dump(increment_all(...$source));
var_dump($source, $copy);
$named = ['left' => 10, 'right' => 20];
var_dump(increment_all(...$named));
var_dump($named);
$first = [30];
$second = [40, 50];
var_dump(increment_all(...$first, ...$second));
var_dump($first, $second);
$external = 60;
$references = [&$external];
increment_all(...$references);
var_dump($external, $references);
// A temporary has no caller-visible slots, but remains a valid unpack.
var_dump(increment_all(...[70, 80]));
}
?>
--EXPECT--
array(2) {
[0]=>
int(0)
[1]=>
int(1)
}
array(2) {
[0]=>
int(2)
[1]=>
int(3)
}
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
array(2) {
[0]=>
string(4) "left"
[1]=>
string(5) "right"
}
array(2) {
["left"]=>
int(11)
["right"]=>
int(21)
}
array(3) {
[0]=>
int(0)
[1]=>
int(1)
[2]=>
int(2)
}
array(1) {
[0]=>
int(31)
}
array(2) {
[0]=>
int(41)
[1]=>
int(51)
}
int(61)
array(1) {
[0]=>
&int(61)
}
array(2) {
[0]=>
int(0)
[1]=>
int(1)
}
Loading…
Cancel
Save