From 0da07cb06505af8dd665ecdbe18975417220c2ef Mon Sep 17 00:00:00 2001 From: Yurun Date: Wed, 15 Jul 2026 19:06:49 +0800 Subject: [PATCH 01/10] =?UTF-8?q?fix(compiler):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=AF=B9=E6=9C=AA=E5=AE=9A=E4=B9=89=E5=8F=98=E9=87=8F=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20=3F=3F=3D=20=E6=97=B6=E7=9A=84=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Parser/AssignOpTrait.php | 14 +++++++++++--- .../coalesce/assign-coalesce-undefined-var.phpt | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 tests/compiler/coalesce/assign-coalesce-undefined-var.phpt diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index cf826761..826cd011 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -862,6 +862,17 @@ trait AssignOpTrait protected function parseAssignOpCoalesce(Expr\AssignOp\Coalesce $expr): string { $this->checkLeftValue($expr->var); + + // PHP 允许对未定义的简单变量使用 ??=(例如 `$a ??= 123`): + // 此时 isset 为 false,直接执行赋值。需要提前声明该局部变量, + // 否则 isset 检查会因变量未定义而报错。此处必须声明为 Type::VAR + // (Variant),使其初值为 NULL,从而 isset 在运行时正确判定为 + // false 并执行赋值;若使用原生类型,isset 恒为 true 会导致取到 + // 未初始化的默认值(如 int(0)、空字符串)。 + if ($this->isVarExpr($expr->var) and !$this->hasVar($this->parseIdentifier($expr->var))) { + $this->addLocalVar($this->parseIdentifier($expr->var), Type::VAR); + } + $isset = $this->parseChainedExpr($expr->var, self::OP_ISSET); $var = $this->parseWritableIdentifier($expr->var); @@ -878,9 +889,6 @@ trait AssignOpTrait if ($this->isVarExpr($expr->expr) and !$this->hasVar($right)) { $this->errorUndefinedVariable($expr->expr); } - if ($this->isVarExpr($expr->var) and !$this->hasVar($var)) { - $this->addLocalVar($var, $this->getNormalAssignType($this->detectTypeOfExpr($expr->expr))); - } return '(' . $isset . '?' . $var . ':(' . $var . ' = ' . $right . '))'; } diff --git a/tests/compiler/coalesce/assign-coalesce-undefined-var.phpt b/tests/compiler/coalesce/assign-coalesce-undefined-var.phpt new file mode 100644 index 00000000..4aad8edb --- /dev/null +++ b/tests/compiler/coalesce/assign-coalesce-undefined-var.phpt @@ -0,0 +1,14 @@ +--TEST-- +assign coalesce on undefined variable +--FILE-- + +--EXPECT-- +int(123) +string(3) "foo" From 72cbdc500a1805dda5180ef0e7c714d47afcba6a Mon Sep 17 00:00:00 2001 From: Yurun Date: Wed, 15 Jul 2026 19:20:57 +0800 Subject: [PATCH 02/10] =?UTF-8?q?fix(compiler):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=A3=B0=E6=98=8E=E5=85=88=E5=90=8E=E5=BD=B1=E5=93=8D=E8=B7=A8?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E6=8E=A5=E5=8F=A3=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Translator.php | 4 +- .../interface-impl-param-type-cross-ns.phpt | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 tests/compiler/namespace/interface-impl-param-type-cross-ns.phpt diff --git a/src/Translator.php b/src/Translator.php index 676618a3..ce370f1e 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -3249,8 +3249,8 @@ CODE; Type::STR => [['kind' => 'isString']], Type::ARRAY => [['kind' => 'isArray']], Type::RESOURCE => [['kind' => 'isResource']], - Type::OBJECT => $arg->class - ? [['kind' => 'instanceof', 'class' => $arg->class]] + Type::OBJECT => $arg->declaredClass + ? [['kind' => 'instanceof', 'class' => $arg->declaredClass]] : [['kind' => 'isObject']], default => null, }; diff --git a/tests/compiler/namespace/interface-impl-param-type-cross-ns.phpt b/tests/compiler/namespace/interface-impl-param-type-cross-ns.phpt new file mode 100644 index 00000000..acaec9e6 --- /dev/null +++ b/tests/compiler/namespace/interface-impl-param-type-cross-ns.phpt @@ -0,0 +1,48 @@ +--TEST-- +Cross-namespace interface implementation with an interface-typed parameter must not be reported as incompatible +--FILE-- +test(new \B\Impl1())); + echo "done\n"; + } +} +?> +--EXPECT-- +bool(true) +done From f9516c43c98226a4d33cb0d3377feadbdab58dbe Mon Sep 17 00:00:00 2001 From: Yurun Date: Wed, 15 Jul 2026 19:47:04 +0800 Subject: [PATCH 03/10] =?UTF-8?q?fix(compiler):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E4=B8=ADself=E8=BF=94=E5=9B=9E=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E8=A7=A3=E6=9E=90=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/CompilerBase.php | 5 ++- .../class/interface-method-self-return.phpt | 42 +++++++++++++++++++ .../interface-self-return-namespaced.phpt | 36 ++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 tests/compiler/class/interface-method-self-return.phpt create mode 100644 tests/compiler/namespace/interface-self-return-namespaced.phpt diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 074a1517..fab95bf8 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1017,7 +1017,10 @@ class CompilerBase implements PropertyAccessContext protected function getFullClassName(): string { - return ltrim($this->namespace . '\\' . $this->class, '\\'); + // 在接口上下文中,$this->class 为空但 $this->interface 已设置, + // `self` 类型声明应解析为接口自身的完整名称。 + $classLike = $this->class !== '' ? $this->class : $this->interface; + return ltrim($this->namespace . '\\' . $classLike, '\\'); } protected function getFullMethodName(string $fullClassName, string $method): string diff --git a/tests/compiler/class/interface-method-self-return.phpt b/tests/compiler/class/interface-method-self-return.phpt new file mode 100644 index 00000000..90e44eab --- /dev/null +++ b/tests/compiler/class/interface-method-self-return.phpt @@ -0,0 +1,42 @@ +--TEST-- +interface method with `self` return type implemented by class (fluent interface), and namespace block containing comments +--FILE-- +value = $value; + return $this; + } + } + + function main() + { + $test = new TestClass; + // get() returns self, so the result still satisfies the interface + var_dump($test->get() instanceof TestInterface); + var_dump($test === $test->get()); + // fluent chaining of self-returning methods + var_dump($test->get()->setValue(42)->value); + } +} +?> +--EXPECT-- +bool(true) +bool(true) +int(42) diff --git a/tests/compiler/namespace/interface-self-return-namespaced.phpt b/tests/compiler/namespace/interface-self-return-namespaced.phpt new file mode 100644 index 00000000..cfda0bad --- /dev/null +++ b/tests/compiler/namespace/interface-self-return-namespaced.phpt @@ -0,0 +1,36 @@ +--TEST-- +interface method `self` return type resolves to the interface's fully-qualified name inside a named namespace +--FILE-- +log[] = 'chain'; + return $this; + } + } +} + +namespace { + function main() + { + $w = new \App\Widget(); + var_dump($w->chain()->chain() instanceof \App\Chainable); + var_dump(count($w->log)); + } +} +?> +--EXPECT-- +bool(true) +int(2) From 6daa3121834cf183c859f11755ab1866e7e1c52f Mon Sep 17 00:00:00 2001 From: Yurun Date: Wed, 15 Jul 2026 21:17:52 +0800 Subject: [PATCH 04/10] =?UTF-8?q?fix(compiler):=20=E6=8C=89=E5=80=BC?= =?UTF-8?q?=E6=B6=88=E8=B4=B9=E5=BC=95=E7=94=A8=E8=BF=94=E5=9B=9E=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E6=97=B6=E6=AD=A3=E7=A1=AE=E5=88=86=E7=A6=BB=E5=BC=95?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/CompilerBase.php | 22 +++++++++ src/Generator/CallArgumentGenerator.php | 7 +++ src/Parser/ArrayExpressionTrait.php | 4 +- .../dynamic-return-reference-argument.phpt | 46 +++++++++++++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 tests/compiler/ref/dynamic-return-reference-argument.phpt diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 074a1517..5888454c 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1066,6 +1066,28 @@ class CompilerBase implements PropertyAccessContext return $this->wrapVoidExprAsNull($expr, $this->parseExpr($expr)); } + /** + * 把一个"按引用返回"的调用在按值消费处解引用为值快照。 + * + * 返回引用的调用会产生一个指向被调用方存储的活引用。当该调用被按值消费 + * (例如作为按值函数参数、数组元素、按值返回等会触发 PHP 分离语义的上下文) + * 时,PHP 会在求值那一刻拷贝出值的快照,因此之后对别名存储的修改不应再可见。 + * 我们通过把结果赋值给一个临时 php::Var 来分离引用(普通的 Var 赋值会断开 + * 引用,而 php::Variant(php::Ref) 构造会保留引用),从而保留从左到右的求值顺序。 + * + * 注意:二元/一元运算等操作数上下文应保持引用活动、在运算时读值,不应在此快照; + * 那些上下文由各自的解析器直接保留引用。 + */ + protected function materializeRefReturnAsValue(NodeAbstract $value, string $expr): string + { + if ($value instanceof Expr\CallLike && $this->resolveRefReturningCall($value) !== false) { + $tmpVar = $this->addTmpVar(Type::VAR); + $this->context->beforeStmtLines[] = $tmpVar . ' = ' . $expr . ';'; + return $tmpVar; + } + return $expr; + } + protected function getObjectPropVarName(string $object, string $prop): string { return self::OBJECT_PROP . $object . self::NAMESPACE_SEPARATOR . $prop; diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index 0347667e..79431f24 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -586,6 +586,13 @@ trait CallArgumentGenerator protected function materializeCallArgValue(NodeAbstract $value, string $expr): string { + // A call that returns by reference yields a live php::Ref aliasing the + // callee's storage. When such a call feeds a by-value argument, PHP takes + // a value snapshot at evaluation time (left to right), so later mutations + // to the aliased storage must not be observable. The dynamic ArgList keeps + // references verbatim (Ctor::CopyRef), so we dereference into a temporary + // value at the point of the call. + $expr = $this->materializeRefReturnAsValue($value, $expr); if (!$this->shouldMaterializeCallArg($value)) { return $expr; } diff --git a/src/Parser/ArrayExpressionTrait.php b/src/Parser/ArrayExpressionTrait.php index a920f62b..819cf8cf 100644 --- a/src/Parser/ArrayExpressionTrait.php +++ b/src/Parser/ArrayExpressionTrait.php @@ -60,7 +60,7 @@ trait ArrayExpressionTrait $this->indentLevel++; foreach ($items as $item) { $this->assertExprCanBeUsedAsValue($item->value, 'array value'); - $value = $this->parseIdentifier($item->value); + $value = $this->materializeRefReturnAsValue($item->value, $this->parseIdentifier($item->value)); if ($item->key) { $this->assertExprCanBeUsedAsValue($item->key, 'array key'); $key = $this->parseArrayKey($item->key); @@ -221,7 +221,7 @@ trait ArrayExpressionTrait } $value = $this->convertToRef($item->value); } else { - $value = $this->parseIdentifier($item->value); + $value = $this->materializeRefReturnAsValue($item->value, $this->parseIdentifier($item->value)); } if ($item->unpack) { $this->context->beforeStmtLines[] = $this->getIndent() . $tmpVar . '.merge(' . $value . ');'; diff --git a/tests/compiler/ref/dynamic-return-reference-argument.phpt b/tests/compiler/ref/dynamic-return-reference-argument.phpt new file mode 100644 index 00000000..471881ef --- /dev/null +++ b/tests/compiler/ref/dynamic-return-reference-argument.phpt @@ -0,0 +1,46 @@ +--TEST-- +Reference-returning calls are copied by value when used as call arguments or array elements +--FILE-- + +--EXPECT-- +int(1) +int(2) +int(2) +int(2) +int(0) +int(0) +int(1) +int(2) +array(2) { + [0]=> + int(3) + [1]=> + int(4) +} From 00023cc3decbb39eabf6c8fc6aece5dd35ef165c Mon Sep 17 00:00:00 2001 From: Yurun Date: Sun, 19 Jul 2026 08:55:16 +0800 Subject: [PATCH 05/10] =?UTF-8?q?fix(compiler):=20=E6=8B=92=E7=BB=9D?= =?UTF-8?q?=E6=89=80=E6=9C=89$$=E8=AF=AD=E6=B3=95=E8=80=8C=E9=9D=9E?= =?UTF-8?q?=E4=BB=85=E7=89=B9=E5=AE=9A=E8=A1=A8=E8=BE=BE=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpunit/code/variable-variable-arraydim.php | 8 ++++++++ phpunit/src/VariableVariableTest.php | 9 +++++++++ src/CompilerBase.php | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 phpunit/code/variable-variable-arraydim.php create mode 100644 phpunit/src/VariableVariableTest.php diff --git a/phpunit/code/variable-variable-arraydim.php b/phpunit/code/variable-variable-arraydim.php new file mode 100644 index 00000000..9a5db13f --- /dev/null +++ b/phpunit/code/variable-variable-arraydim.php @@ -0,0 +1,8 @@ + 'hello']; + ${$foo['bar']} = 'world'; + echo $hello; +} diff --git a/phpunit/src/VariableVariableTest.php b/phpunit/src/VariableVariableTest.php new file mode 100644 index 00000000..f3ae8102 --- /dev/null +++ b/phpunit/src/VariableVariableTest.php @@ -0,0 +1,9 @@ +exec('The `$$` syntax is not supported', 'variable-variable-arraydim.php'); + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 074a1517..6381503d 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1273,7 +1273,7 @@ class CompilerBase implements PropertyAccessContext protected function parseVariable(Variable $expr): string { - if (is_object($expr->name) and $this->isVarExpr($expr->name)) { + if (is_object($expr->name)) { $this->fatalError($expr, 'The `$$` syntax is not supported'); } if ($this->isSuperGlobal($expr->name)) { From 8b76f841adad95c52fb3dfb868a4812889098d87 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sun, 19 Jul 2026 10:49:57 +0800 Subject: [PATCH 06/10] test(compiler): strengthen undefined coalesce assignment coverage --- src/Parser/AssignOpTrait.php | 15 ++++++--------- .../coalesce/assign-coalesce-undefined-var.phpt | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 826cd011..dabef684 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -863,19 +863,16 @@ trait AssignOpTrait { $this->checkLeftValue($expr->var); - // PHP 允许对未定义的简单变量使用 ??=(例如 `$a ??= 123`): - // 此时 isset 为 false,直接执行赋值。需要提前声明该局部变量, - // 否则 isset 检查会因变量未定义而报错。此处必须声明为 Type::VAR - // (Variant),使其初值为 NULL,从而 isset 在运行时正确判定为 - // false 并执行赋值;若使用原生类型,isset 恒为 true 会导致取到 - // 未初始化的默认值(如 int(0)、空字符串)。 - if ($this->isVarExpr($expr->var) and !$this->hasVar($this->parseIdentifier($expr->var))) { - $this->addLocalVar($this->parseIdentifier($expr->var), Type::VAR); + // An undefined variable must exist before generating its isset check. + // Keep it as Variant so NULL remains distinguishable from native defaults. + $var = $this->isVarExpr($expr->var) ? $this->parseIdentifier($expr->var) : null; + if ($var !== null && !$this->hasVar($var)) { + $this->addLocalVar($var, Type::VAR); } $isset = $this->parseChainedExpr($expr->var, self::OP_ISSET); - $var = $this->parseWritableIdentifier($expr->var); + $var ??= $this->parseWritableIdentifier($expr->var); $propertyWriteTarget = $this->preparePropertyWriteTarget($expr->var); if ($propertyWriteTarget !== null) { diff --git a/tests/compiler/coalesce/assign-coalesce-undefined-var.phpt b/tests/compiler/coalesce/assign-coalesce-undefined-var.phpt index 4aad8edb..27dd886b 100644 --- a/tests/compiler/coalesce/assign-coalesce-undefined-var.phpt +++ b/tests/compiler/coalesce/assign-coalesce-undefined-var.phpt @@ -8,7 +8,21 @@ var_dump($a); $b ??= 'foo'; $b ??= 'bar'; var_dump($b); + +$c ??= null; +var_dump(isset($c)); +$c ??= 'after-null'; +var_dump($c); + +for ($i = 0; $i < 2; $i++) { + $d ??= printf("default\n"); +} +var_dump($d); ?> --EXPECT-- int(123) string(3) "foo" +bool(false) +string(10) "after-null" +default +int(8) From e231aa2404f2b1eced506e825484c6fdb98abab4 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sun, 19 Jul 2026 10:58:13 +0800 Subject: [PATCH 07/10] test(compiler): cover cross-namespace parameter variance --- src/Translator.php | 5 +++-- .../interface-impl-param-type-cross-ns.phpt | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Translator.php b/src/Translator.php index ce370f1e..e7c3838a 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -3242,6 +3242,7 @@ CODE; return $arg->typeCheck; } + $declaredClass = $arg->declaredClass ?: $arg->class; return match ($arg->type) { Type::INT => [['kind' => 'isInt']], Type::FLOAT => [['kind' => 'isFloat']], @@ -3249,8 +3250,8 @@ CODE; Type::STR => [['kind' => 'isString']], Type::ARRAY => [['kind' => 'isArray']], Type::RESOURCE => [['kind' => 'isResource']], - Type::OBJECT => $arg->declaredClass - ? [['kind' => 'instanceof', 'class' => $arg->declaredClass]] + Type::OBJECT => $declaredClass + ? [['kind' => 'instanceof', 'class' => $declaredClass]] : [['kind' => 'isObject']], default => null, }; diff --git a/tests/compiler/namespace/interface-impl-param-type-cross-ns.phpt b/tests/compiler/namespace/interface-impl-param-type-cross-ns.phpt index acaec9e6..535212a7 100644 --- a/tests/compiler/namespace/interface-impl-param-type-cross-ns.phpt +++ b/tests/compiler/namespace/interface-impl-param-type-cross-ns.phpt @@ -1,8 +1,9 @@ --TEST-- -Cross-namespace interface implementation with an interface-typed parameter must not be reported as incompatible +Cross-namespace interface implementation parameter compatibility is declaration-order independent --FILE-- test(new \B\Impl1())); + var_dump($obj->testParent(new \B\Impl1())); echo "done\n"; } } ?> --EXPECT-- bool(true) +bool(true) done From 6162185bd76c283c2a669370d24949e19c7fd5cc Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sun, 19 Jul 2026 11:04:28 +0800 Subject: [PATCH 08/10] fix(compiler): scope interface self type resolution --- src/CompilerBase.php | 11 ++++++---- src/Generator/TypeCheckGenerator.php | 2 +- src/Resolver/NameResolutionTrait.php | 2 +- .../interface-self-return-namespaced.phpt | 20 +++++++++++++++++++ 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/CompilerBase.php b/src/CompilerBase.php index fab95bf8..310a79b7 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1017,10 +1017,13 @@ class CompilerBase implements PropertyAccessContext protected function getFullClassName(): string { - // 在接口上下文中,$this->class 为空但 $this->interface 已设置, - // `self` 类型声明应解析为接口自身的完整名称。 - $classLike = $this->class !== '' ? $this->class : $this->interface; - return ltrim($this->namespace . '\\' . $classLike, '\\'); + return ltrim($this->namespace . '\\' . $this->class, '\\'); + } + + protected function getFullClassLikeName(): string + { + $name = $this->class !== '' ? $this->class : $this->interface; + return ltrim($this->namespace . '\\' . $name, '\\'); } protected function getFullMethodName(string $fullClassName, string $method): string diff --git a/src/Generator/TypeCheckGenerator.php b/src/Generator/TypeCheckGenerator.php index 16dc4ae3..292967a1 100644 --- a/src/Generator/TypeCheckGenerator.php +++ b/src/Generator/TypeCheckGenerator.php @@ -104,7 +104,7 @@ trait TypeCheckGenerator } if ($name === 'self') { - $class = $this->getFullClassName(); + $class = $this->getFullClassLikeName(); } elseif ($name === 'parent') { $class = $this->classDef->extends ?? ''; } elseif ($name === 'static') { diff --git a/src/Resolver/NameResolutionTrait.php b/src/Resolver/NameResolutionTrait.php index 307117e5..827db4a3 100644 --- a/src/Resolver/NameResolutionTrait.php +++ b/src/Resolver/NameResolutionTrait.php @@ -166,7 +166,7 @@ trait NameResolutionTrait return $this->getTypeFromZendType($typeNameLower); } else { if ($typeName === 'self') { - $class = $this->getFullClassName(); + $class = $this->getFullClassLikeName(); } elseif ($typeName === 'parent') { if (!$this->classDef) { $this->fatalError($type, 'Cannot use "parent" type declaration outside a class'); diff --git a/tests/compiler/namespace/interface-self-return-namespaced.phpt b/tests/compiler/namespace/interface-self-return-namespaced.phpt index cfda0bad..4d05a98a 100644 --- a/tests/compiler/namespace/interface-self-return-namespaced.phpt +++ b/tests/compiler/namespace/interface-self-return-namespaced.phpt @@ -7,6 +7,10 @@ namespace App { interface Chainable { public function chain(): self; + + public function maybe(bool $present): ?self; + + public function combine(self $other): self; } // comment inside a named namespace block (Stmt_Nop) @@ -19,6 +23,16 @@ namespace App { $this->log[] = 'chain'; return $this; } + + public function maybe(bool $present): ?self + { + return $present ? $this : null; + } + + public function combine(Chainable $other): self + { + return $this; + } } } @@ -28,9 +42,15 @@ namespace { $w = new \App\Widget(); var_dump($w->chain()->chain() instanceof \App\Chainable); var_dump(count($w->log)); + var_dump($w->maybe(true) instanceof \App\Chainable); + var_dump($w->maybe(false)); + var_dump($w->combine(new \App\Widget()) === $w); } } ?> --EXPECT-- bool(true) int(2) +bool(true) +NULL +bool(true) From 848c763c0dfa63ea33a4207074377fa56c23cec6 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sun, 19 Jul 2026 11:09:22 +0800 Subject: [PATCH 09/10] test(compiler): cover expression variable names --- phpunit/code/variable-variable-function-call.php | 12 ++++++++++++ phpunit/src/VariableVariableTest.php | 5 +++++ src/CompilerBase.php | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 phpunit/code/variable-variable-function-call.php diff --git a/phpunit/code/variable-variable-function-call.php b/phpunit/code/variable-variable-function-call.php new file mode 100644 index 00000000..26fd0ef1 --- /dev/null +++ b/phpunit/code/variable-variable-function-call.php @@ -0,0 +1,12 @@ +exec('The `$$` syntax is not supported', 'variable-variable-arraydim.php'); } + + public function testVariableVariableWithFunctionCallThrowsUnsupportedError(): void + { + $this->exec('The `$$` syntax is not supported', 'variable-variable-function-call.php'); + } } diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 6381503d..eff7eab5 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1273,7 +1273,7 @@ class CompilerBase implements PropertyAccessContext protected function parseVariable(Variable $expr): string { - if (is_object($expr->name)) { + if (!is_string($expr->name)) { $this->fatalError($expr, 'The `$$` syntax is not supported'); } if ($this->isSuperGlobal($expr->name)) { From 1128fb06c822daa7e0a1d24ad8556e35a36bb949 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sun, 19 Jul 2026 11:15:56 +0800 Subject: [PATCH 10/10] fix(compiler): preserve ref snapshot evaluation order --- src/CompilerBase.php | 17 +++------- .../dynamic-return-reference-argument.phpt | 34 +++++++++++++++++++ 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 5888454c..e92ffb80 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1067,23 +1067,16 @@ class CompilerBase implements PropertyAccessContext } /** - * 把一个"按引用返回"的调用在按值消费处解引用为值快照。 - * - * 返回引用的调用会产生一个指向被调用方存储的活引用。当该调用被按值消费 - * (例如作为按值函数参数、数组元素、按值返回等会触发 PHP 分离语义的上下文) - * 时,PHP 会在求值那一刻拷贝出值的快照,因此之后对别名存储的修改不应再可见。 - * 我们通过把结果赋值给一个临时 php::Var 来分离引用(普通的 Var 赋值会断开 - * 引用,而 php::Variant(php::Ref) 构造会保留引用),从而保留从左到右的求值顺序。 - * - * 注意:二元/一元运算等操作数上下文应保持引用活动、在运算时读值,不应在此快照; - * 那些上下文由各自的解析器直接保留引用。 + * Snapshot a reference-returning call before a by-value container can retain + * its php::Ref. Assigning to an existing Var detaches the reference, unlike + * constructing a Variant directly from Ref. Keep the assignment inline so + * earlier arguments or array elements retain PHP's evaluation order. */ protected function materializeRefReturnAsValue(NodeAbstract $value, string $expr): string { if ($value instanceof Expr\CallLike && $this->resolveRefReturningCall($value) !== false) { $tmpVar = $this->addTmpVar(Type::VAR); - $this->context->beforeStmtLines[] = $tmpVar . ' = ' . $expr . ';'; - return $tmpVar; + return '(' . $tmpVar . ' = ' . $expr . ')'; } return $expr; } diff --git a/tests/compiler/ref/dynamic-return-reference-argument.phpt b/tests/compiler/ref/dynamic-return-reference-argument.phpt index 471881ef..aede4d2a 100644 --- a/tests/compiler/ref/dynamic-return-reference-argument.phpt +++ b/tests/compiler/ref/dynamic-return-reference-argument.phpt @@ -14,6 +14,9 @@ function main() var_dump($v1, $v2); var_dump(test1(), test2()); var_dump([test1(), test2()]); + var_dump(['first' => test1(), test2()]); + var_dump(value_order('arg-left'), ref_order('arg-ref')); + var_dump([value_order('array-left'), ref_order('array-ref')]); } function &test1() @@ -28,6 +31,19 @@ function &test2() ++$value; return $value; } + +function value_order(string $label): string +{ + echo "$label\n"; + return $label; +} + +function &ref_order(string $label) +{ + static $value = 42; + echo "$label\n"; + return $value; +} ?> --EXPECT-- int(1) @@ -44,3 +60,21 @@ array(2) { [1]=> int(4) } +array(2) { + ["first"]=> + int(5) + [0]=> + int(6) +} +arg-left +arg-ref +string(8) "arg-left" +int(42) +array-left +array-ref +array(2) { + [0]=> + string(10) "array-left" + [1]=> + int(42) +}