From 0e6d65c6e13b9428f5d17b0975482370affc08cc Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 28 Aug 2026 20:01:11 +0800 Subject: [PATCH] feat: support static by-reference variadics --- docs/en/INCOMPATIBLE_PHP_FEATURES.md | 10 +- docs/en/PHP_INCOMPATIBILITY_CLASSIFICATION.md | 6 +- docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md | 4 +- phpunit/src/FunctionTest.php | 4 +- phpunit/src/NegativeCompatibilityTest.php | 73 +++++++----- src/Generator/CallArgumentGenerator.php | 29 +++-- src/Generator/ClosureGenerator.php | 59 +++++++--- src/Generator/TypeCheckGenerator.php | 9 +- src/Parser/TypeConversionTrait.php | 10 ++ src/Preprocessor.php | 10 +- src/Translator.php | 4 +- tests/compiler/SKIP_TESTS.md | 9 +- .../closure/by-reference-parameters.phpt | 45 +++++++ tests/compiler/ref/ref-closure-param.phpt | 25 ++-- .../compiler/variadic/by-reference-basic.phpt | 111 ++++++++++++++++++ .../by-reference-closure-callback.phpt | 49 ++++++++ .../by-reference-dynamic-explicit.phpt | 50 ++++++++ .../variadic/by-reference-inheritance.phpt | 39 ++++++ .../compiler/variadic/by-reference-types.phpt | 93 +++++++++++++++ .../variadic/by-reference-unpack.phpt | 99 ++++++++++++++++ 20 files changed, 646 insertions(+), 92 deletions(-) create mode 100644 tests/compiler/closure/by-reference-parameters.phpt create mode 100644 tests/compiler/variadic/by-reference-basic.phpt create mode 100644 tests/compiler/variadic/by-reference-closure-callback.phpt create mode 100644 tests/compiler/variadic/by-reference-dynamic-explicit.phpt create mode 100644 tests/compiler/variadic/by-reference-inheritance.phpt create mode 100644 tests/compiler/variadic/by-reference-types.phpt create mode 100644 tests/compiler/variadic/by-reference-unpack.phpt diff --git a/docs/en/INCOMPATIBLE_PHP_FEATURES.md b/docs/en/INCOMPATIBLE_PHP_FEATURES.md index e9d7819b..dbb7404e 100644 --- a/docs/en/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/en/INCOMPATIBLE_PHP_FEATURES.md @@ -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 diff --git a/docs/en/PHP_INCOMPATIBILITY_CLASSIFICATION.md b/docs/en/PHP_INCOMPATIBILITY_CLASSIFICATION.md index 4c0ffe57..97c19173 100644 --- a/docs/en/PHP_INCOMPATIBILITY_CLASSIFICATION.md +++ b/docs/en/PHP_INCOMPATIBILITY_CLASSIFICATION.md @@ -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. | diff --git a/docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md b/docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md index 983721aa..7f00ac71 100644 --- a/docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/zh-cn/INCOMPATIBLE_PHP_FEATURES.md @@ -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()` 只接受变量、数组元素或对象属性。 diff --git a/phpunit/src/FunctionTest.php b/phpunit/src/FunctionTest.php index 85f03046..e7c1b2c6 100644 --- a/phpunit/src/FunctionTest.php +++ b/phpunit/src/FunctionTest.php @@ -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() diff --git a/phpunit/src/NegativeCompatibilityTest.php b/phpunit/src/NegativeCompatibilityTest.php index 5fdfc65a..6a69a1b5 100644 --- a/phpunit/src/NegativeCompatibilityTest.php +++ b/phpunit/src/NegativeCompatibilityTest.php @@ -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' [ - 'convert', - 'Closure cannot use reference parameter', + yield 'property get hook reference return' => [ + 'prepare', + 'Property get hooks returning by reference are not supported', <<<'PHP' $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' $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' $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' $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' 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); diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index 3ed87898..99428229 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -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 ''; } diff --git a/src/Generator/TypeCheckGenerator.php b/src/Generator/TypeCheckGenerator.php index b6e16df6..c99fbd62 100644 --- a/src/Generator/TypeCheckGenerator.php +++ b/src/Generator/TypeCheckGenerator.php @@ -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)) { diff --git a/src/Parser/TypeConversionTrait.php b/src/Parser/TypeConversionTrait.php index 62a86066..5e7da7fc 100644 --- a/src/Parser/TypeConversionTrait.php +++ b/src/Parser/TypeConversionTrait.php @@ -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; diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 2007a9db..f824e9a7 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -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']; diff --git a/src/Translator.php b/src/Translator.php index b894b7cb..dac9b883 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -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'); diff --git a/tests/compiler/SKIP_TESTS.md b/tests/compiler/SKIP_TESTS.md index 80aa0dd1..0703cfca 100644 --- a/tests/compiler/SKIP_TESTS.md +++ b/tests/compiler/SKIP_TESTS.md @@ -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` - **详细说明**: 所有可执行表达式必须在函数或类的方法中 diff --git a/tests/compiler/closure/by-reference-parameters.phpt b/tests/compiler/closure/by-reference-parameters.phpt new file mode 100644 index 00000000..a82a0c95 --- /dev/null +++ b/tests/compiler/closure/by-reference-parameters.phpt @@ -0,0 +1,45 @@ +--TEST-- +Dynamic Closures accept positional arguments explicitly marked with refval +--FILE-- + ++$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 diff --git a/tests/compiler/ref/ref-closure-param.phpt b/tests/compiler/ref/ref-closure-param.phpt index 03c77079..0e37e70f 100644 --- a/tests/compiler/ref/ref-closure-param.phpt +++ b/tests/compiler/ref/ref-closure-param.phpt @@ -1,21 +1,10 @@ --TEST-- closure function with ref parameter ---SKIPIF-- - --FILE-- '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 (_)" +} diff --git a/tests/compiler/variadic/by-reference-basic.phpt b/tests/compiler/variadic/by-reference-basic.phpt new file mode 100644 index 00000000..d764fa14 --- /dev/null +++ b/tests/compiler/variadic/by-reference-basic.phpt @@ -0,0 +1,111 @@ +--TEST-- +By-reference variadic parameters preserve direct, named and method arguments +--FILE-- +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) diff --git a/tests/compiler/variadic/by-reference-closure-callback.phpt b/tests/compiler/variadic/by-reference-closure-callback.phpt new file mode 100644 index 00000000..39b36795 --- /dev/null +++ b/tests/compiler/variadic/by-reference-closure-callback.phpt @@ -0,0 +1,49 @@ +--TEST-- +Reference Closure parameters work at Zend callback boundaries used by Symfony mbstring polyfill +--FILE-- + '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) +} diff --git a/tests/compiler/variadic/by-reference-dynamic-explicit.phpt b/tests/compiler/variadic/by-reference-dynamic-explicit.phpt new file mode 100644 index 00000000..fcad5efb --- /dev/null +++ b/tests/compiler/variadic/by-reference-dynamic-explicit.phpt @@ -0,0 +1,50 @@ +--TEST-- +Dynamic calls require explicit refval for by-reference arguments +--FILE-- + +--EXPECT-- +int(41) +string(4) "one!" +string(4) "two!" +string(5) "two!?" diff --git a/tests/compiler/variadic/by-reference-inheritance.phpt b/tests/compiler/variadic/by-reference-inheritance.phpt new file mode 100644 index 00000000..cde662bc --- /dev/null +++ b/tests/compiler/variadic/by-reference-inheritance.phpt @@ -0,0 +1,39 @@ +--TEST-- +By-reference variadic signatures remain compatible across interfaces and inheritance +--FILE-- +increment($first, $second); + var_dump($first, $second); +} +?> +--EXPECT-- +int(11) +int(21) diff --git a/tests/compiler/variadic/by-reference-types.phpt b/tests/compiler/variadic/by-reference-types.phpt new file mode 100644 index 00000000..d3ed129d --- /dev/null +++ b/tests/compiler/variadic/by-reference-types.phpt @@ -0,0 +1,93 @@ +--TEST-- +Typed by-reference variadics validate, widen float arguments and write through unions and objects +--FILE-- +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 diff --git a/tests/compiler/variadic/by-reference-unpack.phpt b/tests/compiler/variadic/by-reference-unpack.phpt new file mode 100644 index 00000000..d2775a1c --- /dev/null +++ b/tests/compiler/variadic/by-reference-unpack.phpt @@ -0,0 +1,99 @@ +--TEST-- +By-reference variadic unpack preserves writeback, COW separation, keys and existing references +--FILE-- + 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) +}