From bc799fda0a646efdfc042c2d570a9da4ded6d740 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 7 Jul 2026 11:36:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E9=9D=99=E6=80=81?= =?UTF-8?q?=E5=9B=9E=E8=B0=83=E8=A7=A3=E6=9E=90=E5=8F=8A=E6=8B=BC=E6=8E=A5?= =?UTF-8?q?=E8=B5=8B=E5=80=BC=E7=AD=89=E7=BC=96=E8=AF=91=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Php/CompilerBase.php | 47 +++++++++++ src/Php/Parser/AssignOpTrait.php | 2 +- .../aot/class/class-alias-class-constant.phpt | 38 +++++++++ ...tic-closure-use-ref-internal-callback.phpt | 38 +++++++++ ...static-closure-use-ref-typed-callback.phpt | 47 +++++++++++ .../call-static-magic-forward.phpt | 51 ++++++++++++ .../functions/internal-union-return-type.phpt | 28 +++++++ tests/aot/operator/concat-assign-null.phpt | 20 +++++ .../static-prop-inherited-isolated.phpt | 78 +++++++++++++++++++ 9 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 tests/aot/class/class-alias-class-constant.phpt create mode 100644 tests/aot/closure/static-closure-use-ref-internal-callback.phpt create mode 100644 tests/aot/closure/static-closure-use-ref-typed-callback.phpt create mode 100644 tests/aot/dynamic_call/call-static-magic-forward.phpt create mode 100644 tests/aot/functions/internal-union-return-type.phpt create mode 100644 tests/aot/operator/concat-assign-null.phpt create mode 100644 tests/aot/static/static-prop-inherited-isolated.phpt diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 9e5b9069..a07a0f5e 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -3686,6 +3686,13 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont $this->fatalError($arg, 'Cannot use positional argument after argument unpacking'); } $byRef = $funcName && $this->isReferenceArgument($funcName, $className, $i); + if (($funcName === 'call_user_func' || $funcName === 'call_user_func_array') && $i === 0) { + $callback = $this->parseScopedCallbackArg($arg); + if ($callback !== null) { + $this->addPositionalCallArg($callback, $arrayArgsVar, $list_args); + continue; + } + } if ($this->isVarExpr($arg->value)) { $name = $this->parseIdentifier($arg->value); if ($byRef) { @@ -3771,6 +3778,46 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont return $namedArgsVar !== null ? $callArgs . ', ' . $namedArgsVar . '.array()' : $callArgs; } + protected function parseScopedCallbackArg(Node\Arg $arg): ?string + { + $value = $arg->value; + if (!$value instanceof Expr\Array_ || count($value->items) < 2 || !$this->methodDef) { + return null; + } + + $first = $value->items[0]; + if (!$first instanceof ArrayItem || $first->key !== null || $first->unpack) { + return null; + } + if (!$first->value instanceof Node\Scalar\String_) { + return null; + } + + $scope = strtolower($first->value->value); + $classExpr = match ($scope) { + 'static' => ($this->methodDef->flags & Modifiers::STATIC) + ? $this->getLiteralString($this->getFullClassName()) + : Symbol::getCalledClass(), + 'self' => $this->getLiteralString($this->getFullClassName()), + 'parent' => $this->classDef->extends ? $this->getLiteralString($this->classDef->extends) : null, + default => null, + }; + if ($classExpr === null) { + return null; + } + + $items = [$classExpr]; + foreach (array_slice($value->items, 1) as $item) { + if (!$item instanceof ArrayItem || $item->key !== null || $item->unpack) { + return null; + } + $this->assertExprCanBeUsedAsValue($item->value, 'callback array item'); + $items[] = $this->parseIdentifier($item->value); + } + + return $this->genArray($items); + } + protected function ensureCallArrayArgs(?string &$arrayArgsVar, array &$listArgs): string { if ($arrayArgsVar === null) { diff --git a/src/Php/Parser/AssignOpTrait.php b/src/Php/Parser/AssignOpTrait.php index 4daaa4a5..992a688b 100644 --- a/src/Php/Parser/AssignOpTrait.php +++ b/src/Php/Parser/AssignOpTrait.php @@ -362,7 +362,7 @@ trait AssignOpTrait if ($this->isArrayVar($node->var)) { $this->fatalError($node->var, 'Cannot concat string to array'); } - return $var . '.append(' . $rightExprStr . ')'; + return $var . ' = php::concat(' . $var . ', ' . $rightExprStr . ')'; } if ($this->isAssignOpPow($op)) { $powExpr = 'php::fn::pow(' . $var . ', ' . $rightExprStr . ')'; diff --git a/tests/aot/class/class-alias-class-constant.phpt b/tests/aot/class/class-alias-class-constant.phpt new file mode 100644 index 00000000..1a6efc39 --- /dev/null +++ b/tests/aot/class/class-alias-class-constant.phpt @@ -0,0 +1,38 @@ +--TEST-- +class_alias with ::class constant +--FILE-- +value()); + var_dump(AliasCopy::ok()); +} +?> +--EXPECT-- +bool(true) +bool(true) +string(13) "AliasOriginal" +string(2) "ok" diff --git a/tests/aot/closure/static-closure-use-ref-internal-callback.phpt b/tests/aot/closure/static-closure-use-ref-internal-callback.phpt new file mode 100644 index 00000000..53ed291a --- /dev/null +++ b/tests/aot/closure/static-closure-use-ref-internal-callback.phpt @@ -0,0 +1,38 @@ +--TEST-- +static closure use by reference through internal callback +--FILE-- + +--EXPECT-- +array(3) { + [0]=> + int(10) + [1]=> + int(20) + [2]=> + int(30) +} +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} diff --git a/tests/aot/closure/static-closure-use-ref-typed-callback.phpt b/tests/aot/closure/static-closure-use-ref-typed-callback.phpt new file mode 100644 index 00000000..09cbd468 --- /dev/null +++ b/tests/aot/closure/static-closure-use-ref-typed-callback.phpt @@ -0,0 +1,47 @@ +--TEST-- +static closure use by reference through typed Closure parameter +--FILE-- + +--EXPECT-- +array(3) { + [0]=> + string(3) "v10" + [1]=> + string(3) "v20" + [2]=> + string(3) "v30" +} +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} diff --git a/tests/aot/dynamic_call/call-static-magic-forward.phpt b/tests/aot/dynamic_call/call-static-magic-forward.phpt new file mode 100644 index 00000000..f782aa06 --- /dev/null +++ b/tests/aot/dynamic_call/call-static-magic-forward.phpt @@ -0,0 +1,51 @@ +--TEST-- +__callStatic forwards to static method through call_user_func_array +--FILE-- += $min && $len <= $max; + } +} + +function main() +{ + var_dump(StaticForwarder::__callStatic('allLengthBetween', [['aa', 'bbbb'], 2, 4])); + var_dump(StaticForwarder::__callStatic('allLengthBetween', [['a', 'bbbb'], 2, 4])); + var_dump(StaticForwarder::__callStatic('nullOrLengthBetween', [null, 2, 4])); + var_dump(StaticForwarder::__callStatic('nullOrLengthBetween', ['abc', 2, 4])); +} +?> +--EXPECT-- +bool(true) +bool(false) +bool(true) +bool(true) diff --git a/tests/aot/functions/internal-union-return-type.phpt b/tests/aot/functions/internal-union-return-type.phpt new file mode 100644 index 00000000..10ebcc5e --- /dev/null +++ b/tests/aot/functions/internal-union-return-type.phpt @@ -0,0 +1,28 @@ +--TEST-- +internal function with union return type does not crash type detection +--FILE-- + 0); + + $tz = new DateTimeZone('UTC'); + $transitions = $tz->getTransitions(0, 1); + var_dump(is_array($transitions)); +} +?> +--EXPECT-- +bool(true) +false branch +bool(true) +bool(true) diff --git a/tests/aot/operator/concat-assign-null.phpt b/tests/aot/operator/concat-assign-null.phpt new file mode 100644 index 00000000..b669f751 --- /dev/null +++ b/tests/aot/operator/concat-assign-null.phpt @@ -0,0 +1,20 @@ +--TEST-- +concat assignment treats null as empty string +--FILE-- + +--EXPECT-- +string(4) "a123" +string(1) "b" diff --git a/tests/aot/static/static-prop-inherited-isolated.phpt b/tests/aot/static/static-prop-inherited-isolated.phpt new file mode 100644 index 00000000..43587b80 --- /dev/null +++ b/tests/aot/static/static-prop-inherited-isolated.phpt @@ -0,0 +1,78 @@ +--TEST-- +inherited static property accessed through static:: remains isolated per class +--FILE-- + +--EXPECT-- +array(3) { + [0]=> + string(16) "StaticOptionBase" + [1]=> + bool(true) + [2]=> + array(0) { + } +} +array(3) { + [0]=> + string(18) "StaticOptionChildA" + [1]=> + bool(false) + [2]=> + array(2) { + [0]=> + string(21) "StaticOptionChildA:on" + [1]=> + string(22) "StaticOptionChildA:off" + } +} +array(3) { + [0]=> + string(18) "StaticOptionChildB" + [1]=> + bool(false) + [2]=> + array(1) { + [0]=> + string(22) "StaticOptionChildB:off" + } +}