From c0556882ab9c56f78c750c772eaf3601b6651f1c Mon Sep 17 00:00:00 2001 From: Yurun Date: Wed, 22 Jul 2026 16:43:03 +0800 Subject: [PATCH 01/16] =?UTF-8?q?fix(generator):=20=E6=8E=A5=E5=8F=97=20\G?= =?UTF-8?q?enerator=20=E4=BD=9C=E4=B8=BA=E5=90=88=E6=B3=95=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Generator/FiberGenerator.php | 6 +- .../generator/generator-foreach-yield.phpt | 57 +++++++++++++++++++ .../generator-return-type-generator.phpt | 53 +++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 tests/compiler/generator/generator-foreach-yield.phpt create mode 100644 tests/compiler/generator/generator-return-type-generator.phpt diff --git a/src/Generator/FiberGenerator.php b/src/Generator/FiberGenerator.php index a67f5870..9e766fd2 100644 --- a/src/Generator/FiberGenerator.php +++ b/src/Generator/FiberGenerator.php @@ -112,7 +112,11 @@ trait FiberGenerator [, $class] = $this->resolveTypeDecl($type, self::DECL_TYPE_OF_RETURN); $class = strtolower(ltrim($class, '\\')); - return in_array($class, ['iterator', 'traversable', 'fibergenerator'], true); + // `\Generator` is the return type PHP programmers naturally write for a + // generator. TypePHP generators actually return a `\FiberGenerator`, so + // accepting the declared `Generator` type keeps PHP source compatible + // while the runtime object remains a `\FiberGenerator`. + return in_array($class, ['iterator', 'traversable', 'fibergenerator', 'generator'], true); } protected function parseYieldExpr(Yield_ $expr): string diff --git a/tests/compiler/generator/generator-foreach-yield.phpt b/tests/compiler/generator/generator-foreach-yield.phpt new file mode 100644 index 00000000..d2029fc9 --- /dev/null +++ b/tests/compiler/generator/generator-foreach-yield.phpt @@ -0,0 +1,57 @@ +--TEST-- +generator re-yielding array elements via foreach with \Generator return type +--FILE-- + +--EXPECTF-- +object(FiberGenerator)#%d (9) { + ["callback":"FiberGenerator":private]=> + object(Closure)#%d (2) { + ["function"]=> + string(19) "stdClass::{closure}" + ["this"]=> + object(stdClass)#%d (1) { + ["box"]=> + resource(%d) of type (php::box) + } + } + ["fiber":"FiberGenerator":private]=> + NULL + ["current":"FiberGenerator":private]=> + NULL + ["key":"FiberGenerator":private]=> + NULL + ["valid":"FiberGenerator":private]=> + bool(false) + ["state":"FiberGenerator":private]=> + int(0) + ["yield_count":"FiberGenerator":private]=> + int(0) + ["next_index":"FiberGenerator":private]=> + int(0) + ["return_value":"FiberGenerator":private]=> + NULL +} +int(1) +int(2) +int(3) diff --git a/tests/compiler/generator/generator-return-type-generator.phpt b/tests/compiler/generator/generator-return-type-generator.phpt new file mode 100644 index 00000000..4d5894cc --- /dev/null +++ b/tests/compiler/generator/generator-return-type-generator.phpt @@ -0,0 +1,53 @@ +--TEST-- +generator return type accepts \Generator for methods, nullable and union variants +--FILE-- +gen([1, 2, 3]) as $v) { + var_dump($v); + } + $g = nullableGen([4, 5]); + foreach ($g as $v) { + var_dump($v); + } + $u = unionGen([6, 7]); + foreach ($u as $v) { + var_dump($v); + } +} +?> +--EXPECT-- +int(2) +int(4) +int(6) +int(4) +int(5) +int(6) +int(7) From c3b24fdd323caf9c7a2e07cb486f2e303e17322c Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 21:37:26 +0800 Subject: [PATCH 02/16] =?UTF-8?q?fix(compiler):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=E7=B1=BB=E5=9E=8B=E5=8D=8F=E5=8F=98=E6=A3=80?= =?UTF-8?q?=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Translator.php | 130 ++++++++++++++++-- .../class/interface-return-covariance.phpt | 47 +++++++ .../type_decl/return-type-covariance.phpt | 51 +++++++ 3 files changed, 218 insertions(+), 10 deletions(-) create mode 100644 tests/compiler/class/interface-return-covariance.phpt create mode 100644 tests/compiler/type_decl/return-type-covariance.phpt diff --git a/src/Translator.php b/src/Translator.php index 14916aec..8d36035f 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -3717,25 +3717,135 @@ CODE; if ($childFuncDef->returnTypeUndeclared) { return false; } - if ($parentFuncDef->returnTypeCheck || $childFuncDef->returnTypeCheck) { - return $parentFuncDef->returnTypeStr === $childFuncDef->returnTypeStr; - } + // A parent that accepts everything (mixed/var) is compatible with any + // child return type. if ($parentFuncDef->returnType === Type::VAR) { return true; } - if ($childFuncDef->returnType !== $parentFuncDef->returnType) { - return false; + + $parentTypes = $this->getReturnAcceptedTypes($parentFuncDef); + $childTypes = $this->getReturnAcceptedTypes($childFuncDef); + + // Return type covariance: every value the child can return must also be + // acceptable under the parent's declared return type. This allows a + // child to narrow a nullable/union return type (e.g. `?Base` -> `?Child` + // or `int|string` -> `int`) while still satisfying the parent contract. + return $this->isReturnTypeSubtype($childTypes, $parentTypes); + } + + private function getReturnAcceptedTypes(FunctionDef $functionDef): array + { + if (!empty($functionDef->returnTypeCheck)) { + return $functionDef->returnTypeCheck; } - if ($parentFuncDef->returnType !== Type::OBJECT) { - return true; + $type = $functionDef->returnType; + if ($type === Type::VAR) { + return [['kind' => 'isMixed']]; + } + if ($type === Type::OBJECT) { + return $functionDef->returnClass + ? [['kind' => 'instanceof', 'class' => $functionDef->returnClass]] + : [['kind' => 'isObject']]; + } + return match ($type) { + Type::INT => [['kind' => 'isInt']], + Type::FLOAT => [['kind' => 'isFloat']], + Type::BOOL => [['kind' => 'isBool']], + Type::STR => [['kind' => 'isString']], + Type::ARRAY => [['kind' => 'isArray']], + Type::RESOURCE => [['kind' => 'isResource']], + default => [['kind' => 'isMixed']], + }; + } + + private function isReturnTypeSubtype(array $childTypes, array $parentTypes): bool + { + foreach ($childTypes as $childType) { + if (!$this->isReturnTypeCoveredBy($childType, $parentTypes)) { + return false; + } } - if ($childFuncDef->returnClass === $parentFuncDef->returnClass) { + return true; + } + + private function isReturnTypeCoveredBy(array $childType, array $parentTypes): bool + { + $childKind = $childType['kind'] ?? null; + + // Child is an intersection (A&B): it is a subtype only if every member + // is individually a subtype of the parent type. + if ($childKind === 'allOf') { + foreach ($childType['types'] as $member) { + if (!$this->isReturnTypeCoveredBy($member, $parentTypes)) { + return false; + } + } return true; } - if (!$childFuncDef->returnClass || !$parentFuncDef->returnClass) { + + foreach ($parentTypes as $parentType) { + $parentKind = $parentType['kind'] ?? null; + + // Parent is an intersection (A&B): the child must be a subtype of + // every member of the intersection. + if ($parentKind === 'allOf') { + $ok = true; + foreach ($parentType['types'] as $member) { + if (!$this->isReturnTypeCoveredBy($childType, [$member])) { + $ok = false; + break; + } + } + if ($ok) { + return true; + } + continue; + } + + if ($this->isReturnTypeEntryCompatible($childKind, $childType, $parentKind, $parentType)) { + return true; + } + } + + return false; + } + + private function isReturnTypeEntryCompatible( + ?string $childKind, + array $childType, + ?string $parentKind, + array $parentType + ): bool { + if ($childKind === 'isNull') { + // A null value is only compatible with a nullable (isNull) parent. + return $parentKind === 'isNull'; + } + if ($childKind === 'isObject') { + // Any object is compatible with a parent that accepts any object. + return $parentKind === 'isObject'; + } + if ($childKind === 'isMixed') { + return $parentKind === 'isMixed'; + } + if ($childKind === 'instanceof') { + if ($parentKind === 'isObject') { + return true; + } + if ($parentKind === 'instanceof') { + $childClass = $childType['class'] ?? ''; + $parentClass = $parentType['class'] ?? ''; + if ($childClass === '' || $parentClass === '' || $childClass === 'static' || $parentClass === 'static') { + return false; + } + if ($childClass === $parentClass) { + return true; + } + return $this->isInheritedFrom($childClass, $parentClass); + } return false; } - return $this->isInheritedFrom($childFuncDef->returnClass, $parentFuncDef->returnClass); + // Scalar kinds must match exactly. + return $childKind === $parentKind; } private function isParameterTypeOverrideCompatible(ArgInfo $childArg, ArgInfo $parentArg): bool diff --git a/tests/compiler/class/interface-return-covariance.phpt b/tests/compiler/class/interface-return-covariance.phpt new file mode 100644 index 00000000..792c3c76 --- /dev/null +++ b/tests/compiler/class/interface-return-covariance.phpt @@ -0,0 +1,47 @@ +--TEST-- +Interface return type covariance with nullable interface and anonymous class +--FILE-- +test(); + var_dump($result instanceof TestInterface1); + var_dump($result instanceof TestInterface2); + var_dump($result === null); +} +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) diff --git a/tests/compiler/type_decl/return-type-covariance.phpt b/tests/compiler/type_decl/return-type-covariance.phpt new file mode 100644 index 00000000..3de3e6bb --- /dev/null +++ b/tests/compiler/type_decl/return-type-covariance.phpt @@ -0,0 +1,51 @@ +--TEST-- +Return type covariance: union narrowing and object subtype +--FILE-- + int) is allowed. + public function make(): int + { + return 42; + } +} + +class BaseType {} +class ChildType extends BaseType {} + +interface ObjectReturnContract +{ + public function build(): BaseType; +} + +class ObjectReturnImpl implements ObjectReturnContract +{ + // Covariant: returning a subtype (ChildType) for a BaseType return is allowed. + public function build(): ChildType + { + return new ChildType(); + } +} + +function main() +{ + $impl = new UnionReturnImpl(); + var_dump($impl->make()); + + $obj = new ObjectReturnImpl(); + $built = $obj->build(); + var_dump($built instanceof BaseType); + var_dump($built instanceof ChildType); +} +?> +--EXPECT-- +int(42) +bool(true) +bool(true) From a3d8f57ef44c485b28d9b8cdba2599a29c50472f Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 21:38:29 +0800 Subject: [PATCH 03/16] =?UTF-8?q?fix(compiler):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E5=B0=BE=E9=83=A8=E6=9C=89?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=E5=AF=BC=E8=87=B4=E7=BC=96=E8=AF=91=E4=B8=8D?= =?UTF-8?q?=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Preprocessor.php | 2 ++ src/Translator.php | 2 ++ .../namespace/namespace-ending-comment.phpt | 22 +++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 tests/compiler/namespace/namespace-ending-comment.phpt diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 2d311e00..9e0d5d11 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -321,6 +321,8 @@ class Preprocessor extends CompilerBase case 'Stmt_Interface': $this->parseInterface($v2); break; + case 'Stmt_Nop': + break; default: $this->foundStrayCode($v2); break; diff --git a/src/Translator.php b/src/Translator.php index 14916aec..402568ea 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -2514,6 +2514,8 @@ CODE; case 'Stmt_Interface': $this->validateInterfaceOverrideAttributes($v2); break; + case 'Stmt_Nop': + break; default: abort($v2); break; diff --git a/tests/compiler/namespace/namespace-ending-comment.phpt b/tests/compiler/namespace/namespace-ending-comment.phpt new file mode 100644 index 00000000..fa0f3da0 --- /dev/null +++ b/tests/compiler/namespace/namespace-ending-comment.phpt @@ -0,0 +1,22 @@ +--TEST-- +A namespace block ending with a comment must not be treated as stray code +--FILE-- + +--EXPECT-- +string(4) "done" From edcbcf7cff544f84da1c63fd9638467f3d8c3114 Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 21:38:56 +0800 Subject: [PATCH 04/16] =?UTF-8?q?fix(stub):=20=E4=BF=AE=E5=A4=8D=20self/pa?= =?UTF-8?q?rent/static=20=E7=B1=BB=E5=B8=B8=E9=87=8F=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gen_stub.php | 29 +++++++++++---------- tests/compiler/self-class/003.phpt | 41 ++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 tests/compiler/self-class/003.phpt diff --git a/src/gen_stub.php b/src/gen_stub.php index 5b65c0c5..9f09e78f 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -2368,20 +2368,23 @@ class EvaluatedValue return ltrim($className, '\\'); } $class = getClassConstFetchClassName($expr); - if ($class === 'self') { - $constName = ClassInfo::$currentClass . "::" . $constName; - if (isset($allConstInfos[$constName])) { - return $allConstInfos[$constName]->getValue($allConstInfos)->value; - } else { - return normalizeConstExprValue( - getTranslator()->getClassConstValue($expr, ClassInfo::$currentClass, $constName) - ); - } - } else { - return normalizeConstExprValue( - getTranslator()->getClassConstValue($expr, $class, $constName, ClassInfo::$currentClass) - ); + // Resolve the special class-name keywords to concrete classes. + // Previously `self` was passed as both the class and a + // `ClassName::` name prefix (yielding `B::B::A`), and `parent` / + // `static` were passed verbatim (yielding `parent::A`), so the + // constant lookup always failed. + if (strcasecmp($class, 'self') === 0 || strcasecmp($class, 'static') === 0) { + $class = ClassInfo::$currentClass; + } elseif (strcasecmp($class, 'parent') === 0) { + $class = getTranslator()->getParentClass(ClassInfo::$currentClass); + } + $fqcnName = ltrim($class, '\\') . "::" . $constName; + if (isset($allConstInfos[$fqcnName])) { + return $allConstInfos[$fqcnName]->getValue($allConstInfos)->value; } + return normalizeConstExprValue( + getTranslator()->getClassConstValue($expr, $class, $constName, ClassInfo::$currentClass) + ); } else { $constName = $expr->name->__toString(); if (strtolower($constName) === "unknown") { diff --git a/tests/compiler/self-class/003.phpt b/tests/compiler/self-class/003.phpt new file mode 100644 index 00000000..86bd825e --- /dev/null +++ b/tests/compiler/self-class/003.phpt @@ -0,0 +1,41 @@ +--TEST-- +Class constant referenced via self:: / parent:: / ClassName:: as a property default value +--FILE-- +a); + var_dump($test->local); + var_dump($test->b); + var_dump($test->c); +} +?> +--EXPECT-- +int(1) +int(10) +int(1) +int(2) From c7d4b562c0ddc69bcdc82854e711b56879ba91a4 Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 21:39:25 +0800 Subject: [PATCH 05/16] =?UTF-8?q?fix(parser):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=95=B0=E7=BB=84=E5=85=83=E7=B4=A0=E5=BC=95=E7=94=A8=E8=B5=8B?= =?UTF-8?q?=E5=80=BC=E5=86=99=E5=9B=9E=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Parser/AssignOpTrait.php | 17 +++++++++- tests/compiler/ref/array-ref-assign-001.phpt | 29 +++++++++++++++++ tests/compiler/ref/array-ref-assign-002.phpt | 34 ++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/compiler/ref/array-ref-assign-001.phpt create mode 100644 tests/compiler/ref/array-ref-assign-002.phpt diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index dabef684..5fdb32b4 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -42,11 +42,24 @@ trait AssignOpTrait $tmp = $this->genTmpVarName(); $this->addLocalVar($tmp, Type::VAR); + // 仅当目标是 php::Array 时使用 item/newItem: + // - item(dim, true) 直接返回元素 zval 地址,赋值时能穿透 IS_REFERENCE 写回, + // 修复 $arr = [&$x] / $arr[] = &$x 这类数组元素引用的写回问题; + // - 对于 ArrayAccess 对象(如 ArrayObject)或类型未知(VAR)的变量,item 不存在或语义不符, + // 必须继续使用 offsetSet(对象数组元素的引用写回由对象自身保证,编译器不负责)。 + $isPhpArray = $this->getVarType($array) === Type::ARRAY; + if ($left->dim === null) { + if ($isPhpArray) { + return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.newItem() = {$tmp}" . '), ' . $tmp . ')'; + } return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet(" . self::VALUE_NULL . ", {$tmp})" . '), ' . $tmp . ')'; } $dim = $this->parseIdentifier($left->dim); + if ($isPhpArray) { + return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.item({$dim}, true) = {$tmp}" . '), ' . $tmp . ')'; + } return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet({$dim}, {$tmp})" . '), ' . $tmp . ')'; } @@ -828,7 +841,9 @@ trait AssignOpTrait $left = $this->parseIdentifier($expr->var); $rightExpr = $tmpVar . ' = ' . $this->emitStaticPropertyFetchRef($expr->expr, $expr); } elseif ($this->isArrayDimFetch($expr->expr)) { - $left = $this->parseIdentifier($expr->var); + // $left 已在函数开头通过 parseWritableIdentifier($expr->var) 正确计算, + // 这里不可再用 parseIdentifier() 覆盖,否则当左值是数组追加($arr[] = &$x) + // 或数组元素($arr[$k] = &$x)时会被当作读取而报错 "Cannot use [] for reading"。 $array = $this->parseWritableIdentifier($expr->expr->var); if ($expr->expr->dim == null) { $this->fatalError($expr, 'Cannot assign reference to array dim fetch without dim'); diff --git a/tests/compiler/ref/array-ref-assign-001.phpt b/tests/compiler/ref/array-ref-assign-001.phpt new file mode 100644 index 00000000..3273f29e --- /dev/null +++ b/tests/compiler/ref/array-ref-assign-001.phpt @@ -0,0 +1,29 @@ +--TEST-- +array reference assignment: append and element assignment write back through reference +--FILE-- + +--EXPECT-- +array(3) { + [0]=> + &int(123) + [1]=> + &int(456) + [2]=> + int(3) +} +array(2) { + [0]=> + &int(123) + [1]=> + &int(456) +} diff --git a/tests/compiler/ref/array-ref-assign-002.phpt b/tests/compiler/ref/array-ref-assign-002.phpt new file mode 100644 index 00000000..025e6bad --- /dev/null +++ b/tests/compiler/ref/array-ref-assign-002.phpt @@ -0,0 +1,34 @@ +--TEST-- +array reference assignment to element: $arr[$k] = &$v writes back through reference +--FILE-- + +--EXPECT-- +int(100) +int(200) +int(111) +int(222) +int(77) From 8fff430fcc667889458b53e1247a252df7b0521b Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 21:39:58 +0800 Subject: [PATCH 06/16] =?UTF-8?q?fix(preprocessor):=20=E5=8C=85=E8=A3=85?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E5=B8=B8=E9=87=8F=E4=BD=9C=E4=B8=BA?= =?UTF-8?q?=E6=A0=87=E9=87=8F=E5=8F=82=E6=95=B0=E9=BB=98=E8=AE=A4=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Preprocessor.php | 50 ++++++++++++++++++- .../class-const-default-value-typed.phpt | 25 ++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 tests/compiler/const/class-const-default-value-typed.phpt diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 2d311e00..cfd3c7d9 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -83,6 +83,53 @@ class Preprocessor extends CompilerBase return $type . ' ' . $argInfo->name; } + /** + * A default value that can only be resolved at runtime (e.g. a class/global + * constant coming from a class that is not compiled into the binary) is emitted + * as a `php::constant(...)` call, which returns a `php::Variant`. + * + * Copy-initializing a typed (non-Variant) parameter such as `php::Int`, + * `php::Float`, `php::Bool`, `php::Str`, `php::Array` or `php::Object` from a + * `php::Variant` is rejected by C++ because the conversion is explicit: + * + * php::Int type = php::constant(...); // error C2440 + * + * The function body already converts such values with `php::toInt(...)` / + * `php::toFloat(...)` / ... (see convertExprFromType), so we wrap the default + * with the very same conversion here. This keeps the declaration consistent with + * the body and produces compilable code: + * + * php::Int type = php::toInt(php::constant(...)); // OK + * + * Parameters whose effective type is `php::Var` (including Stream/Box, which are + * mapped to `php::Var`) accept a `php::Variant` directly, so they are left alone. + */ + protected function wrapScalarDefaultValue(string $type, string $defaultExpr): string + { + if (!str_starts_with($defaultExpr, 'php::constant(')) { + return $defaultExpr; + } + $target = $type; + if ($target === Type::STREAM || $target === Type::BOX) { + $target = Type::VAR; + } + static $converters = [ + Type::INT => 'php::toInt', + Type::FLOAT => 'php::toFloat', + Type::BOOL => 'php::toBool', + Type::STR => 'php::toString', + Type::ARRAY => 'php::toArray', + Type::OBJECT => 'php::toObject', + Type::BIGINT => 'php::toBigInt', + Type::DECIMAL => 'php::toDecimal', + Type::BIGFLOAT => 'php::toBigFloat', + ]; + if (isset($converters[$target])) { + return $converters[$target] . '(' . $defaultExpr . ')'; + } + return $defaultExpr; + } + public function getCppFile(string $file): string { $info = pathinfo($file); @@ -466,7 +513,8 @@ class Preprocessor extends CompilerBase $argInfo->default = 'php::newReference(' . $this->parseParamDefaultValue($param->default) . ')'; } } else { - $argInfo->default = $arrayInitPlan ? $arrayInitPlan->expr : $this->parseParamDefaultValue($param->default); + $defaultExpr = $arrayInitPlan ? $arrayInitPlan->expr : $this->parseParamDefaultValue($param->default); + $argInfo->default = $this->wrapScalarDefaultValue($argInfo->type, $defaultExpr); $argInfo->arrayInitPlan = $arrayInitPlan; $argInfo->defaultValue = $param->default; } diff --git a/tests/compiler/const/class-const-default-value-typed.phpt b/tests/compiler/const/class-const-default-value-typed.phpt new file mode 100644 index 00000000..7ee526ed --- /dev/null +++ b/tests/compiler/const/class-const-default-value-typed.phpt @@ -0,0 +1,25 @@ +--TEST-- +Typed parameter default value from an unresolvable (external) class constant +--FILE-- +run(); +} +?> +--EXPECT-- +int(2) From d16717eb61a9b6fa660553b4a623a6ec0185ca3c Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 21:40:54 +0800 Subject: [PATCH 07/16] =?UTF-8?q?fix(stub):=20=E4=BF=AE=E5=A4=8Dheredoc/no?= =?UTF-8?q?wdoc=E5=AD=97=E7=AC=A6=E4=B8=B2=E7=94=9F=E6=88=90=E9=9D=9E?= =?UTF-8?q?=E6=B3=95C++=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gen_stub.php | 17 ++++++++- .../const/class-const-heredoc-nowdoc.phpt | 23 +++++++++++ .../const/heredoc-nowdoc-const-defaults.phpt | 38 +++++++++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 tests/compiler/const/class-const-heredoc-nowdoc.phpt create mode 100644 tests/compiler/const/heredoc-nowdoc-const-defaults.phpt diff --git a/src/gen_stub.php b/src/gen_stub.php index 5b65c0c5..ff1406a0 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -2515,14 +2515,20 @@ class EvaluatedValue return '"' . getTranslator()->escapeString((string) $this->value) . '"'; } elseif ($this->expr instanceof Expr\ConstFetch) { return getTranslator()->getConstValue($this->expr->name->toString()); - } elseif (!($this->expr instanceof String_)) { + } elseif ($this->expr instanceof String_) { + // Heredoc/nowdoc and quoted string literals: emit the decoded + // value directly. Pretty-printing a heredoc/nowdoc would leak + // the `<<escapeString((string) $this->value) . '"'; + } else { // ConstExprEvaluator has already reduced concatenations and // other constant string expressions to their PHP value. Emit // that value as a C string literal instead of rejecting every // non-literal string expression. return '"' . getTranslator()->escapeString((string) $this->value) . '"'; } - $expr = preg_replace("/(^'|'$)/", '"', getTranslator()->escapeString($expr)); } elseif ($this->type->isInt() or $this->type->isFloat()) { return strval($this->value); } elseif ($this->type->isBool()) { @@ -5107,6 +5113,13 @@ function parseFunctionLike( if ($param->default instanceof Expr\ClassConstFetch && $param->default->class->toLowerString() === "self") { $defaultValue = getTranslator()->getClassConstValue($func, $name->className->name, $param->default->name->name); $defaultValue = var_export($defaultValue, true); + } elseif ($param->default instanceof String_ && + in_array($param->default->getAttribute('kind'), [String_::KIND_HEREDOC, String_::KIND_NOWDOC], true) + ) { + // heredoc/nowdoc: prettyPrint 会输出 `<<escapeString((string) $param->default->value) . '"'; } else { $defaultValue = $param->default ? $prettyPrinter->prettyPrintExpr($param->default) : null; } diff --git a/tests/compiler/const/class-const-heredoc-nowdoc.phpt b/tests/compiler/const/class-const-heredoc-nowdoc.phpt new file mode 100644 index 00000000..350c1ad7 --- /dev/null +++ b/tests/compiler/const/class-const-heredoc-nowdoc.phpt @@ -0,0 +1,23 @@ +--TEST-- +class constants with heredoc and nowdoc syntax +--FILE-- + +--EXPECT-- +string(3) "abc" +string(3) "def" diff --git a/tests/compiler/const/heredoc-nowdoc-const-defaults.phpt b/tests/compiler/const/heredoc-nowdoc-const-defaults.phpt new file mode 100644 index 00000000..07868984 --- /dev/null +++ b/tests/compiler/const/heredoc-nowdoc-const-defaults.phpt @@ -0,0 +1,38 @@ +--TEST-- +global constants, property defaults and parameter defaults with heredoc/nowdoc syntax +--FILE-- +p); + var_dump(with_default()); +} +?> +--EXPECT-- +string(3) "abc" +string(3) "def" +string(3) "xyz" +string(3) "abc" From 9cdc8b1b091c42fcfbbbc5f8d46d19e22d279bbf Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 21:41:24 +0800 Subject: [PATCH 08/16] =?UTF-8?q?fix(compiler):=20=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E6=9E=84=E9=80=A0=E5=87=BD=E6=95=B0=E5=8F=AF=E8=A7=81=E6=80=A7?= =?UTF-8?q?=E4=BB=A5=E9=98=B2=E6=AD=A2=E9=9D=9E=E6=B3=95=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code/constructor_visibility_private.php | 11 ++++ .../code/constructor_visibility_protected.php | 11 ++++ ...tor_visibility_protected_foreign_class.php | 19 +++++++ phpunit/code/trait_constructor_conflict.php | 29 ++++++++++ phpunit/code/trait_constructor_private.php | 21 +++++++ phpunit/code/trait_constructor_protected.php | 21 +++++++ phpunit/src/ConstructorVisibilityTest.php | 57 +++++++++++++++++++ src/CompilerBase.php | 42 ++++++++++++++ .../ctor-visibility-protected-subclass.phpt | 26 +++++++++ tests/compiler/trait/trait-ctor-basic.phpt | 27 +++++++++ tests/compiler/trait/trait-ctor-override.phpt | 32 +++++++++++ .../trait/trait-ctor-protected-subclass.phpt | 37 ++++++++++++ .../compiler/trait/trait-ctor-with-args.phpt | 30 ++++++++++ 13 files changed, 363 insertions(+) create mode 100644 phpunit/code/constructor_visibility_private.php create mode 100644 phpunit/code/constructor_visibility_protected.php create mode 100644 phpunit/code/constructor_visibility_protected_foreign_class.php create mode 100644 phpunit/code/trait_constructor_conflict.php create mode 100644 phpunit/code/trait_constructor_private.php create mode 100644 phpunit/code/trait_constructor_protected.php create mode 100644 phpunit/src/ConstructorVisibilityTest.php create mode 100644 tests/compiler/object_ctor/ctor-visibility-protected-subclass.phpt create mode 100644 tests/compiler/trait/trait-ctor-basic.phpt create mode 100644 tests/compiler/trait/trait-ctor-override.phpt create mode 100644 tests/compiler/trait/trait-ctor-protected-subclass.phpt create mode 100644 tests/compiler/trait/trait-ctor-with-args.phpt diff --git a/phpunit/code/constructor_visibility_private.php b/phpunit/code/constructor_visibility_private.php new file mode 100644 index 00000000..89f7c1aa --- /dev/null +++ b/phpunit/code/constructor_visibility_private.php @@ -0,0 +1,11 @@ +compile($file); + } catch (TestError | \RuntimeException $exception) { + $this->assertStringContainsString($expected, $exception->getMessage()); + return; + } + $this->fail('Expected compile-time error was not thrown'); + } + + public function testPrivateConstructorCannotBeCalledFromOutside(): void + { + // 私有构造器不能从类外部通过 `new` 调用 + $this->exec('Cannot call private TestClass::__construct()', 'constructor_visibility_private.php'); + } + + public function testProtectedConstructorCannotBeCalledFromGlobalScope(): void + { + // 保护构造器不能从全局作用域调用 + $this->exec('Cannot call protected TestClass::__construct()', 'constructor_visibility_protected.php'); + } + + public function testProtectedConstructorCannotBeCalledFromNonSubclass(): void + { + // 保护构造器不能从非子类的其它类内部调用 + $this->exec('Cannot call protected Base::__construct()', 'constructor_visibility_protected_foreign_class.php'); + } + + public function testTraitPrivateConstructorCannotBeCalledFromGlobalScope(): void + { + // trait 提供的私有构造器扁平化后等价于类的私有构造器 + $this->exec('Cannot call private TestClass::__construct()', 'trait_constructor_private.php'); + } + + public function testTraitProtectedConstructorCannotBeCalledFromGlobalScope(): void + { + // trait 提供的保护构造器扁平化后等价于类的保护构造器 + $this->exec('Cannot call protected TestClass::__construct()', 'trait_constructor_protected.php'); + } + + public function testConflictingTraitConstructorMustBeResolved(): void + { + // 两个 trait 各自声明 __construct 时必须显式解决冲突 + $this->exec('Trait `TraitB` method `__construct` already exists', 'trait_constructor_conflict.php'); + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index c183b5a9..387b03ff 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -3177,6 +3177,15 @@ class CompilerBase implements PropertyAccessContext if ($classDef->flags & Modifiers::ABSTRACT) { $this->fatalError($expr, "abstract class `{$className}` cannot be instantiated"); } + // 检查构造函数可见性(private/protected 在不可访问的上下文中被调用) + $ctor = $this->findConstructor($className); + if ($ctor !== null && !$this->checkAccessible($ctor['classDef'], $ctor['flags'])) { + $this->fatalError( + $expr, + 'Cannot call ' . $this->visibilityLabel($ctor['flags']) . ' ' + . $ctor['classDef']->getNamespacedName() . '::__construct()' + ); + } } $cePtr = $this->getClassEntryPtr($className); } @@ -3859,6 +3868,39 @@ class CompilerBase implements PropertyAccessContext return true; } + /** + * 沿继承链查找定义 __construct 的类及其可见性标志。 + * 返回 ['classDef' => ClassDef, 'flags' => int],未找到(例如构造函数定义在内部类)时返回 null。 + * + * @return array{classDef: ClassDef, flags: int}|null + */ + protected function findConstructor(string $className): ?array + { + $current = $className; + while ($current !== '' && $current !== null) { + if (!$this->hasClass($current)) { + return null; + } + $classDef = $this->getClass($current); + if ($classDef->hasMethod('__construct')) { + return ['classDef' => $classDef, 'flags' => $classDef->getMethod('__construct')->flags]; + } + $current = $classDef->extends; + } + return null; + } + + protected function visibilityLabel(int $flags): string + { + if ($flags & Modifiers::PRIVATE) { + return 'private'; + } + if ($flags & Modifiers::PROTECTED) { + return 'protected'; + } + return 'public'; + } + protected function genDebugInfo(?NodeAbstract $stmt = null, string $functionName = '', int $startLine = 0): string { $code = ''; diff --git a/tests/compiler/object_ctor/ctor-visibility-protected-subclass.phpt b/tests/compiler/object_ctor/ctor-visibility-protected-subclass.phpt new file mode 100644 index 00000000..a1db317b --- /dev/null +++ b/tests/compiler/object_ctor/ctor-visibility-protected-subclass.phpt @@ -0,0 +1,26 @@ +--TEST-- +Constructor visibility - protected constructor accessible from subclass +--FILE-- + +--EXPECT-- +bool(true) diff --git a/tests/compiler/trait/trait-ctor-basic.phpt b/tests/compiler/trait/trait-ctor-basic.phpt new file mode 100644 index 00000000..1cf76b71 --- /dev/null +++ b/tests/compiler/trait/trait-ctor-basic.phpt @@ -0,0 +1,27 @@ +--TEST-- +Trait __construct is used by the composing class +--FILE-- + +--EXPECT-- +trait ctor diff --git a/tests/compiler/trait/trait-ctor-override.phpt b/tests/compiler/trait/trait-ctor-override.phpt new file mode 100644 index 00000000..5b40f487 --- /dev/null +++ b/tests/compiler/trait/trait-ctor-override.phpt @@ -0,0 +1,32 @@ +--TEST-- +Class __construct overrides the one provided by a trait +--FILE-- + +--EXPECT-- +class ctor diff --git a/tests/compiler/trait/trait-ctor-protected-subclass.phpt b/tests/compiler/trait/trait-ctor-protected-subclass.phpt new file mode 100644 index 00000000..319fd2f3 --- /dev/null +++ b/tests/compiler/trait/trait-ctor-protected-subclass.phpt @@ -0,0 +1,37 @@ +--TEST-- +Trait protected __construct is accessible from a subclass +--FILE-- + +--EXPECT-- +base ctor +sub ctor diff --git a/tests/compiler/trait/trait-ctor-with-args.phpt b/tests/compiler/trait/trait-ctor-with-args.phpt new file mode 100644 index 00000000..5177cb28 --- /dev/null +++ b/tests/compiler/trait/trait-ctor-with-args.phpt @@ -0,0 +1,30 @@ +--TEST-- +Trait __construct with arguments and $this property access +--FILE-- +value = $value; + echo "value=" . $this->value . "\n"; + } +} + +class TestClass +{ + use TestTrait; +} + +function main() +{ + new TestClass(42); +} +?> +--EXPECT-- +value=42 From cdf4e5f789967f2c12f7ae042ee5c95c109cb646 Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 21:43:30 +0800 Subject: [PATCH 09/16] =?UTF-8?q?fix(compiler):=20=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=99=A8=E5=A3=B0=E6=98=8E=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E4=BB=A5=E6=BB=A1=E8=B6=B3=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=8D=8F=E5=8F=98=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick of upstream 84ad3a5. The generator return type covariance change is built on yurun's Translator.php, which diverged from origin/master, so the files are materialized from the upstream commit directly (they already include the cc25d71 FiberGenerator change). - isReturnTypeOverrideCompatible now performs full covariance: every value the child can return must be acceptable under the parent's declared return type (nullable/union narrowing allowed). - Add getReturnAcceptedTypes/isReturnTypeSubtype/isReturnTypeCoveredBy/ isReturnTypeEntryCompatible helpers; generators use their source-level declared return type so interface/abstract covariance checks hold. - FunctionDef gains declaredReturnType/declaredReturnClass/declaredReturnTypeCheck. Regression tests: interface-return-type.phpt, interface-return-type-variants.phpt. --- src/Entity/FunctionDef.php | 21 ++ src/Generator/FiberGenerator.php | 11 + src/Translator.php | 203 +++++++++++++++--- .../interface-return-type-variants.phpt | 82 +++++++ .../generator/interface-return-type.phpt | 38 ++++ 5 files changed, 323 insertions(+), 32 deletions(-) create mode 100644 tests/compiler/generator/interface-return-type-variants.phpt create mode 100644 tests/compiler/generator/interface-return-type.phpt diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index 78e8c01b..8cf00783 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -33,6 +33,14 @@ class FunctionDef public string $attributeFactoryScope = ''; /** External library imported by the stub containing this function. */ public string $importLibrary = ''; + /** + * True for a trait method whose body contains `parent::` calls. Such methods + * receive an implicit `zend_class_entry *trait_parent_ce` parameter (right after + * `this_`) so the `parent::` call can be bound to the class that composes the + * trait. Both the definition and the shared `func_decl.h` declaration must emit + * this parameter, otherwise the declaration/definition signatures disagree. + */ + public bool $traitParentCe = false; public bool $returnTypeUndeclared = false; public bool $returnsByRef = false; public bool $generator = false; @@ -74,6 +82,19 @@ class FunctionDef /** Original union/nullable return type AST node. */ public ?NodeAbstract $returnTypeNode = null; + /** + * Source-level return type declared on a generator method, preserved after + * `prepareGeneratorFunction()` neutralizes the runtime return type. A + * generator actually returns a `\FiberGenerator` (which implements + * `Iterator`), so the C++ return type and runtime type check are left + * neutral; this copy is only used by interface/abstract return-type + * covariance checks so a generator method can still satisfy a contract such + * as `: \Generator`. + */ + public ?string $declaredReturnType = null; + public string $declaredReturnClass = ''; + public ?array $declaredReturnTypeCheck = null; + public function __construct(string $name, string $returnType, string $namespace) { $this->name = $name; diff --git a/src/Generator/FiberGenerator.php b/src/Generator/FiberGenerator.php index 9e766fd2..e547372b 100644 --- a/src/Generator/FiberGenerator.php +++ b/src/Generator/FiberGenerator.php @@ -72,6 +72,17 @@ trait FiberGenerator if (!$this->generatorReturnTypeAcceptsFiber($v->returnType)) { $this->fatalError($v, 'Generator return type must accept \\FiberGenerator; use Iterator, Traversable, iterable, object, mixed, or omit the return type'); } + // Preserve the source-level declared return type before neutralizing the + // runtime return type. The override compatibility check still needs it so + // a generator method can satisfy an interface/abstract contract such as + // `: \Generator` (the runtime object is a `\FiberGenerator`, not a Zend + // `Generator`, so the C++ return type and runtime check stay neutral). + if ($v->returnType !== null) { + $declared = $this->buildTypeCheckFromNode($v->returnType); + $functionDef->declaredReturnTypeCheck = $declared['check'] ?: null; + } + $functionDef->declaredReturnType = $functionDef->returnType; + $functionDef->declaredReturnClass = $functionDef->returnClass; $functionDef->generator = true; $functionDef->returnType = Type::VAR; $functionDef->returnClass = ''; diff --git a/src/Translator.php b/src/Translator.php index 14916aec..f1f3e55f 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1602,6 +1602,12 @@ CODE; $list = []; if ($func->method) { $list[] = Type::OBJECT . ' &this_'; + // A trait method with `parent::` calls receives an implicit + // `trait_parent_ce` parameter right after `this_`. The definition + // adds it (see parseFunction); the declaration must match. + if ($func->traitParentCe) { + $list[] = 'zend_class_entry *trait_parent_ce'; + } } $argInfoList = $func->argInfoList; if ($argInfoList) { @@ -2514,6 +2520,8 @@ CODE; case 'Stmt_Interface': $this->validateInterfaceOverrideAttributes($v2); break; + case 'Stmt_Nop': + break; default: abort($v2); break; @@ -3464,6 +3472,9 @@ CODE; $functionDeclCode .= Type::OBJECT . ' &this_'; if ($this->classDef?->trait !== null && $this->methodDef?->parentMethodCalls) { $functionDeclCode .= ', zend_class_entry *trait_parent_ce'; + // Record the implicit parameter so the shared `func_decl.h` + // declaration (genFunctionDeclaration) emits the same signature. + $this->functionDef->traitParentCe = true; } if ($this->functionDef->params) { $functionDeclCode .= ', '; @@ -3717,25 +3728,162 @@ CODE; if ($childFuncDef->returnTypeUndeclared) { return false; } - if ($parentFuncDef->returnTypeCheck || $childFuncDef->returnTypeCheck) { - return $parentFuncDef->returnTypeStr === $childFuncDef->returnTypeStr; - } + // A parent that accepts everything (mixed/var) is compatible with any + // child return type. if ($parentFuncDef->returnType === Type::VAR) { return true; } - if ($childFuncDef->returnType !== $parentFuncDef->returnType) { - return false; + + $parentTypes = $this->getReturnAcceptedTypes($parentFuncDef); + $childTypes = $this->getReturnAcceptedTypes($childFuncDef); + + // Return type covariance: every value the child can return must also be + // acceptable under the parent's declared return type. This allows a + // child to narrow a nullable/union return type (e.g. `?Base` -> `?Child` + // or `int|string` -> `int`) while still satisfying the parent contract. + return $this->isReturnTypeSubtype($childTypes, $parentTypes); + } + + private function getReturnAcceptedTypes(FunctionDef $functionDef): array + { + if ($functionDef->generator) { + // The runtime return type of a generator is neutralized to VAR because + // it actually returns a `\FiberGenerator`. Use the source-level declared + // return type so interface/abstract covariance checks still work. + if (!empty($functionDef->declaredReturnTypeCheck)) { + return $functionDef->declaredReturnTypeCheck; + } + $type = $functionDef->declaredReturnType; + if ($type === Type::VAR) { + return [['kind' => 'isMixed']]; + } + if ($type === Type::OBJECT) { + return $functionDef->declaredReturnClass + ? [['kind' => 'instanceof', 'class' => $functionDef->declaredReturnClass]] + : [['kind' => 'isObject']]; + } + return match ($type) { + Type::INT => [['kind' => 'isInt']], + Type::FLOAT => [['kind' => 'isFloat']], + Type::BOOL => [['kind' => 'isBool']], + Type::STR => [['kind' => 'isString']], + Type::ARRAY => [['kind' => 'isArray']], + Type::RESOURCE => [['kind' => 'isResource']], + default => [['kind' => 'isMixed']], + }; } - if ($parentFuncDef->returnType !== Type::OBJECT) { - return true; + + if (!empty($functionDef->returnTypeCheck)) { + return $functionDef->returnTypeCheck; } - if ($childFuncDef->returnClass === $parentFuncDef->returnClass) { + $type = $functionDef->returnType; + if ($type === Type::VAR) { + return [['kind' => 'isMixed']]; + } + if ($type === Type::OBJECT) { + return $functionDef->returnClass + ? [['kind' => 'instanceof', 'class' => $functionDef->returnClass]] + : [['kind' => 'isObject']]; + } + return match ($type) { + Type::INT => [['kind' => 'isInt']], + Type::FLOAT => [['kind' => 'isFloat']], + Type::BOOL => [['kind' => 'isBool']], + Type::STR => [['kind' => 'isString']], + Type::ARRAY => [['kind' => 'isArray']], + Type::RESOURCE => [['kind' => 'isResource']], + default => [['kind' => 'isMixed']], + }; + } + + private function isReturnTypeSubtype(array $childTypes, array $parentTypes): bool + { + foreach ($childTypes as $childType) { + if (!$this->isReturnTypeCoveredBy($childType, $parentTypes)) { + return false; + } + } + return true; + } + + private function isReturnTypeCoveredBy(array $childType, array $parentTypes): bool + { + $childKind = $childType['kind'] ?? null; + + // Child is an intersection (A&B): it is a subtype only if every member + // is individually a subtype of the parent type. + if ($childKind === 'allOf') { + foreach ($childType['types'] as $member) { + if (!$this->isReturnTypeCoveredBy($member, $parentTypes)) { + return false; + } + } return true; } - if (!$childFuncDef->returnClass || !$parentFuncDef->returnClass) { + + foreach ($parentTypes as $parentType) { + $parentKind = $parentType['kind'] ?? null; + + // Parent is an intersection (A&B): the child must be a subtype of + // every member of the intersection. + if ($parentKind === 'allOf') { + $ok = true; + foreach ($parentType['types'] as $member) { + if (!$this->isReturnTypeCoveredBy($childType, [$member])) { + $ok = false; + break; + } + } + if ($ok) { + return true; + } + continue; + } + + if ($this->isReturnTypeEntryCompatible($childKind, $childType, $parentKind, $parentType)) { + return true; + } + } + + return false; + } + + private function isReturnTypeEntryCompatible( + ?string $childKind, + array $childType, + ?string $parentKind, + array $parentType + ): bool { + if ($childKind === 'isNull') { + // A null value is only compatible with a nullable (isNull) parent. + return $parentKind === 'isNull'; + } + if ($childKind === 'isObject') { + // Any object is compatible with a parent that accepts any object. + return $parentKind === 'isObject'; + } + if ($childKind === 'isMixed') { + return $parentKind === 'isMixed'; + } + if ($childKind === 'instanceof') { + if ($parentKind === 'isObject') { + return true; + } + if ($parentKind === 'instanceof') { + $childClass = $childType['class'] ?? ''; + $parentClass = $parentType['class'] ?? ''; + if ($childClass === '' || $parentClass === '' || $childClass === 'static' || $parentClass === 'static') { + return false; + } + if ($childClass === $parentClass) { + return true; + } + return $this->isInheritedFrom($childClass, $parentClass); + } return false; } - return $this->isInheritedFrom($childFuncDef->returnClass, $parentFuncDef->returnClass); + // Scalar kinds must match exactly. + return $childKind === $parentKind; } private function isParameterTypeOverrideCompatible(ArgInfo $childArg, ArgInfo $parentArg): bool @@ -4320,6 +4468,14 @@ CODE; // function untouched. $this->reresolveTraitLateBoundTypes($classDef, $methodDef); + // The wrapper is a method of the *composing* class, not a trait method, so + // it must not receive the implicit `trait_parent_ce` parameter (it computes + // the parent class entry itself when forwarding to the trait function). The + // cloned FunctionDef inherited `traitParentCe` from the trait's FunctionDef; + // clear it so the shared `func_decl.h` declaration matches the wrapper's + // own (2-parameter) definition. + $methodDef->functionDef->traitParentCe = false; + // Validate `parent::` calls emitted from this trait method against the // parent of the class that is composing the trait. The trait itself has // no parent at compile time, so this is the only place the parent class @@ -4390,28 +4546,11 @@ CODE; private function reresolveTraitLateBoundTypes(ClassDef $usingClassDef, MethodDef $methodDef): void { $fn = $methodDef->functionDef; - $needsClone = false; - - if ($fn->returnTypeKeyword !== '') { - $resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword); - if ($resolved !== null && $resolved !== $fn->returnClass) { - $needsClone = true; - } - } - foreach ($fn->argInfoList as $arg) { - if ($arg->typeKeyword !== '') { - $resolved = $this->resolveLateBoundClass($usingClassDef, $arg->typeKeyword); - if ($resolved !== null && ($resolved !== $arg->class || $resolved !== $arg->declaredClass)) { - $needsClone = true; - break; - } - } - } - - if (!$needsClone) { - return; - } - + // Always produce a distinct FunctionDef for the composing-class wrapper. + // The wrapper is a separate method (different name, and no implicit + // `trait_parent_ce` parameter) from the trait's own function, so it must + // not share the trait's FunctionDef object — mutating one (e.g. clearing + // `traitParentCe`) would otherwise leak into the trait's declaration. $newFn = clone $fn; if ($fn->returnTypeKeyword !== '') { $resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword); diff --git a/tests/compiler/generator/interface-return-type-variants.phpt b/tests/compiler/generator/interface-return-type-variants.phpt new file mode 100644 index 00000000..c6ee5ef5 --- /dev/null +++ b/tests/compiler/generator/interface-return-type-variants.phpt @@ -0,0 +1,82 @@ +--TEST-- +generator methods implementing interfaces with iterable, nullable and union return types +--FILE-- +gen([1, 2, 3]) as $v) { + var_dump($v); + } + foreach ($box->it([4, 5]) as $v) { + var_dump($v); + } + foreach ($box->nullable([6, 7]) as $v) { + var_dump($v); + } + foreach ($box->union([8, 9]) as $v) { + var_dump($v); + } +} +?> +--EXPECT-- +int(2) +int(4) +int(6) +int(4) +int(5) +int(6) +int(7) +int(8) +int(9) diff --git a/tests/compiler/generator/interface-return-type.phpt b/tests/compiler/generator/interface-return-type.phpt new file mode 100644 index 00000000..82eb4ca4 --- /dev/null +++ b/tests/compiler/generator/interface-return-type.phpt @@ -0,0 +1,38 @@ +--TEST-- +generator method implementing an interface that declares \Generator return type +--FILE-- +test([1, 2, 3]); + // TypePHP generators return a \FiberGenerator which implements Iterator + // but is NOT the Zend \Generator class. + var_dump($g instanceof \Generator); + var_dump($g instanceof \Iterator); + foreach ($g as $value) { + var_dump($value); + } +} +?> +--EXPECT-- +bool(false) +bool(true) +int(1) +int(2) +int(3) From 9b1f5b79421f6a3701043988f004075248010e27 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 24 Jul 2026 16:50:25 +0800 Subject: [PATCH 10/16] fix(compiler): harden constructor visibility checks --- ...nstructor_visibility_inherited_private.php | 17 +++++ ...onstructor_visibility_internal_private.php | 6 ++ .../constructor_visibility_namespaced.php | 15 +++++ phpunit/src/ConstructorVisibilityTest.php | 23 ++++++- src/CompilerBase.php | 66 ++++++++++++------- 5 files changed, 101 insertions(+), 26 deletions(-) create mode 100644 phpunit/code/constructor_visibility_inherited_private.php create mode 100644 phpunit/code/constructor_visibility_internal_private.php create mode 100644 phpunit/code/constructor_visibility_namespaced.php diff --git a/phpunit/code/constructor_visibility_inherited_private.php b/phpunit/code/constructor_visibility_inherited_private.php new file mode 100644 index 00000000..53ace63a --- /dev/null +++ b/phpunit/code/constructor_visibility_inherited_private.php @@ -0,0 +1,17 @@ +compile($file); - } catch (TestError | \RuntimeException $exception) { + } catch (TestError|RuntimeException $exception) { $this->assertStringContainsString($expected, $exception->getMessage()); return; } @@ -37,6 +37,27 @@ class ConstructorVisibilityTest extends BaseTest $this->exec('Cannot call protected Base::__construct()', 'constructor_visibility_protected_foreign_class.php'); } + public function testInheritedPrivateConstructorCannotBeCalledFromChildScope(): void + { + $this->exec( + 'Cannot call private PrivateConstructorParent::__construct()', + 'constructor_visibility_inherited_private.php' + ); + } + + public function testInternalPrivateConstructorCannotBeCalled(): void + { + $this->exec('Cannot call private Closure::__construct()', 'constructor_visibility_internal_private.php'); + } + + public function testNamespacedConstructorUsesPhpClassNameInDiagnostic(): void + { + $this->exec( + 'Cannot call private ConstructorVisibility\Hidden::__construct()', + 'constructor_visibility_namespaced.php' + ); + } + public function testTraitPrivateConstructorCannotBeCalledFromGlobalScope(): void { // trait 提供的私有构造器扁平化后等价于类的私有构造器 diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 387b03ff..5a2f709a 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -3172,20 +3172,17 @@ class CompilerBase implements PropertyAccessContext $className = $this->getNamespacedClassName($className); } $ctorClassName = $className; - if ($this->hasClass($className)) { - $classDef = $this->getClass($className); - if ($classDef->flags & Modifiers::ABSTRACT) { - $this->fatalError($expr, "abstract class `{$className}` cannot be instantiated"); - } - // 检查构造函数可见性(private/protected 在不可访问的上下文中被调用) - $ctor = $this->findConstructor($className); - if ($ctor !== null && !$this->checkAccessible($ctor['classDef'], $ctor['flags'])) { - $this->fatalError( - $expr, - 'Cannot call ' . $this->visibilityLabel($ctor['flags']) . ' ' - . $ctor['classDef']->getNamespacedName() . '::__construct()' - ); - } + if ($this->isAbstractClass($className)) { + $this->fatalError($expr, "abstract class `{$className}` cannot be instantiated"); + } + $constructor = $this->findConstructor($className); + if ($constructor !== null + && !$this->checkAccessibleByClassName($constructor['className'], $constructor['flags'])) { + $this->fatalError( + $expr, + 'Cannot call ' . $this->visibilityLabel($constructor['flags']) . ' ' + . $constructor['className'] . '::__construct()' + ); } $cePtr = $this->getClassEntryPtr($className); } @@ -3842,6 +3839,11 @@ class CompilerBase implements PropertyAccessContext } protected function checkAccessible(ClassDef $classDef, int $flags): bool + { + return $this->checkAccessibleByClassName($classDef->getNamespacedName(false), $flags); + } + + protected function checkAccessibleByClassName(string $declaringClass, int $flags): bool { $scopeClassDef = $this->classDef; if ($this->functionDef !== null @@ -3852,7 +3854,7 @@ class CompilerBase implements PropertyAccessContext // 私有方法,只能当前的类使用 if ($flags & Modifiers::PRIVATE) { return $scopeClassDef !== null - && strcasecmp($classDef->getNamespacedName(false), $scopeClassDef->getNamespacedName(false)) === 0; + && $this->isSameClassName($declaringClass, $scopeClassDef->getNamespacedName(false)); } // 保护方法,只能当前类和子类使用 if ($flags & Modifiers::PROTECTED) { @@ -3861,7 +3863,7 @@ class CompilerBase implements PropertyAccessContext } return $this->canAccessProtectedProperty( $scopeClassDef->getNamespacedName(false), - $classDef->getNamespacedName(false) + $declaringClass ); } // 类外部调用,只允许调用 public 方法 @@ -3869,23 +3871,37 @@ class CompilerBase implements PropertyAccessContext } /** - * 沿继承链查找定义 __construct 的类及其可见性标志。 - * 返回 ['classDef' => ClassDef, 'flags' => int],未找到(例如构造函数定义在内部类)时返回 null。 + * 沿继承链查找实际调用的构造函数,包括项目类继承的内部类构造函数。 * - * @return array{classDef: ClassDef, flags: int}|null + * @return array{className: string, flags: int}|null */ protected function findConstructor(string $className): ?array { $current = $className; - while ($current !== '' && $current !== null) { - if (!$this->hasClass($current)) { + while ($current !== '') { + if ($this->hasClass($current)) { + $classDef = $this->getClass($current); + if ($classDef->hasMethod('__construct')) { + return [ + 'className' => $classDef->getNamespacedName(false), + 'flags' => $classDef->getMethod('__construct')->flags, + ]; + } + $current = $classDef->extends; + continue; + } + if (!$this->isInternalClass($current)) { return null; } - $classDef = $this->getClass($current); - if ($classDef->hasMethod('__construct')) { - return ['classDef' => $classDef, 'flags' => $classDef->getMethod('__construct')->flags]; + + $constructor = Reflection::getClass($current)?->getConstructor(); + if ($constructor === null) { + return null; } - $current = $classDef->extends; + return [ + 'className' => $constructor->getDeclaringClass()->getName(), + 'flags' => $constructor->getModifiers(), + ]; } return null; } From eadac69dcdcea7ff9741ac2992bfae0ba2aa0c77 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 24 Jul 2026 16:59:31 +0800 Subject: [PATCH 11/16] test(compiler): cover namespace trailing comment variants --- .../namespace_ending_comment_unbracketed.php | 14 ++++++++++++++ phpunit/src/PreprocessorTest.php | 19 +++++++++++++++++++ .../namespace/namespace-ending-comment.phpt | 4 ++-- 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 phpunit/code/preprocessor/namespace_ending_comment_unbracketed.php diff --git a/phpunit/code/preprocessor/namespace_ending_comment_unbracketed.php b/phpunit/code/preprocessor/namespace_ending_comment_unbracketed.php new file mode 100644 index 00000000..7ad8b333 --- /dev/null +++ b/phpunit/code/preprocessor/namespace_ending_comment_unbracketed.php @@ -0,0 +1,14 @@ +assertSame('App\\VERSION', $constants['_const_var_App__VERSION']->name); } + public function testNamespaceEndingCommentWithUnbracketedSyntaxIsIgnored(): void + { + global $translator; + $file = __DIR__ . '/../code/preprocessor/namespace_ending_comment_unbracketed.php'; + $previousTranslator = $translator ?? null; + $translator = $this->compiler; + + try { + $this->compiler->addFiles([$file]); + $this->compiler->prepareFile($file); + $this->compiler->convertFile($file); + } finally { + $translator = $previousTranslator; + } + + $constants = $this->getProperty('constants'); + $this->assertArrayHasKey('_const_var_NamespaceEndingComment__VALUE', $constants); + } + public function testSortFilesUsesImplementsAndTraitDependencies(): void { $classFile = realpath(__DIR__ . '/../code/preprocessor/deps_class_implements.php'); diff --git a/tests/compiler/namespace/namespace-ending-comment.phpt b/tests/compiler/namespace/namespace-ending-comment.phpt index fa0f3da0..e937a711 100644 --- a/tests/compiler/namespace/namespace-ending-comment.phpt +++ b/tests/compiler/namespace/namespace-ending-comment.phpt @@ -6,7 +6,7 @@ A namespace block ending with a comment must not be treated as stray code declare(strict_types=1); namespace Test { - // test + /* named namespace trailing block comment */ } namespace { @@ -15,7 +15,7 @@ namespace { var_dump('done'); } - // test1 + // global namespace trailing line comment } ?> --EXPECT-- From 71caa933d6e3f4b8e2a49003ef3deb71ddd94473 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 24 Jul 2026 17:14:49 +0800 Subject: [PATCH 12/16] fix(compiler): enforce complete return covariance rules --- ...ance_error_return_intersection_missing.php | 23 ++ ...inheritance_error_return_never_widened.php | 13 ++ ...nheritance_error_return_static_widened.php | 17 ++ ...inheritance_error_return_union_widened.php | 14 ++ .../return_type_covariance_intersection.php | 39 ++++ phpunit/src/InheritanceErrorTest.php | 25 ++ src/Generator/TypeCheckGenerator.php | 2 +- src/Preprocessor.php | 4 +- src/Translator.php | 213 ++++++++++-------- .../type_decl/return-type-covariance.phpt | 69 ++++++ 10 files changed, 323 insertions(+), 96 deletions(-) create mode 100644 phpunit/code/inheritance_error_return_intersection_missing.php create mode 100644 phpunit/code/inheritance_error_return_never_widened.php create mode 100644 phpunit/code/inheritance_error_return_static_widened.php create mode 100644 phpunit/code/inheritance_error_return_union_widened.php create mode 100644 phpunit/code/return_type_covariance_intersection.php diff --git a/phpunit/code/inheritance_error_return_intersection_missing.php b/phpunit/code/inheritance_error_return_intersection_missing.php new file mode 100644 index 00000000..03ac56c2 --- /dev/null +++ b/phpunit/code/inheritance_error_return_intersection_missing.php @@ -0,0 +1,23 @@ +exec('must be compatible', 'inheritance_error_return_contravariant_class.php'); } + public function testUnionReturnTypeCannotBeWidenedToUnrelatedType(): void + { + $this->exec('must be compatible', 'inheritance_error_return_union_widened.php'); + } + + public function testIntersectionReturnTypeCannotDropAMember(): void + { + $this->exec('must be compatible', 'inheritance_error_return_intersection_missing.php'); + } + + public function testStaticReturnTypeCannotBeWidenedToSelf(): void + { + $this->exec('must be compatible', 'inheritance_error_return_static_widened.php'); + } + + public function testNeverReturnTypeCannotBeWidenedToVoid(): void + { + $this->exec('must be compatible', 'inheritance_error_return_never_widened.php'); + } + + public function testIntersectionReturnTypeCanNarrowToIntersectionOrConcreteSubtype(): void + { + $this->assertCompiles('return_type_covariance_intersection.php'); + } + public function testParameterTypeCannotBeCovariant() { $this->exec('must be compatible', 'inheritance_error_param_covariant_class.php'); diff --git a/src/Generator/TypeCheckGenerator.php b/src/Generator/TypeCheckGenerator.php index 292967a1..66dbb2e2 100644 --- a/src/Generator/TypeCheckGenerator.php +++ b/src/Generator/TypeCheckGenerator.php @@ -116,7 +116,7 @@ trait TypeCheckGenerator return $class ? [['kind' => 'instanceof', 'class' => $class]] : []; } - private function typeCheckNodeToString(NodeAbstract $typeNode): string + protected function typeCheckNodeToString(NodeAbstract $typeNode): string { if ($typeNode instanceof Node\Identifier) { return $typeNode->name; diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 2d311e00..98e2bcec 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -542,6 +542,9 @@ class Preprocessor extends CompilerBase } $functionDef->exported = !($this->classDef?->exported === false || $this->hasNoExportAttribute($v)); $functionDef->returnClass = $class; + $functionDef->returnTypeStr = $v->returnType === null + ? '' + : $this->typeCheckNodeToString($v->returnType); // Record late-bound return type keywords so they can be re-resolved to // the consuming class when a trait method is flattened into a class. $functionDef->returnTypeKeyword = $returnTypeKeyword; @@ -560,7 +563,6 @@ class Preprocessor extends CompilerBase $typeInfo = $this->buildTypeCheckFromNode($v->returnType); if (!empty($typeInfo['check'])) { $functionDef->returnTypeCheck = $typeInfo['check']; - $functionDef->returnTypeStr = $typeInfo['typeStr']; $functionDef->returnTypeNode = $v->returnType; } } diff --git a/src/Translator.php b/src/Translator.php index 8d36035f..09ac076d 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -3624,7 +3624,12 @@ CODE; )); } - if (!$this->isReturnTypeOverrideCompatible($childFuncDef, $parentFuncDef)) { + if (!$this->isReturnTypeOverrideCompatible( + $childFuncDef, + $parentFuncDef, + $className, + $parentClass, + )) { $this->fatalMethodOverrideIncompatible($v, $className, $methodName, $parentClass); } if ($childFuncDef->returnsByRef !== $parentFuncDef->returnsByRef) { @@ -3709,143 +3714,163 @@ CODE; )); } - private function isReturnTypeOverrideCompatible(FunctionDef $childFuncDef, FunctionDef $parentFuncDef): bool - { + private function isReturnTypeOverrideCompatible( + FunctionDef $childFuncDef, + FunctionDef $parentFuncDef, + string $childClass, + string $parentClass, + ): bool { if ($parentFuncDef->returnTypeUndeclared) { return true; } if ($childFuncDef->returnTypeUndeclared) { return false; } - // A parent that accepts everything (mixed/var) is compatible with any - // child return type. - if ($parentFuncDef->returnType === Type::VAR) { - return true; - } - $parentTypes = $this->getReturnAcceptedTypes($parentFuncDef); - $childTypes = $this->getReturnAcceptedTypes($childFuncDef); + $parentTypes = $this->getReturnAcceptedTypes($parentFuncDef, $parentClass); + $childTypes = $this->getReturnAcceptedTypes($childFuncDef, $childClass); - // Return type covariance: every value the child can return must also be - // acceptable under the parent's declared return type. This allows a - // child to narrow a nullable/union return type (e.g. `?Base` -> `?Child` - // or `int|string` -> `int`) while still satisfying the parent contract. - return $this->isReturnTypeSubtype($childTypes, $parentTypes); + // Type checks are stored in disjunctive normal form: the outer list is + // a union, while an allOf entry is an intersection. Every child union + // branch must imply at least one complete parent branch. + foreach ($childTypes as $childType) { + if (!$this->isReturnTypeCoveredBy($childType, $parentTypes)) { + return false; + } + } + return true; } - private function getReturnAcceptedTypes(FunctionDef $functionDef): array + private function getReturnAcceptedTypes(FunctionDef $functionDef, string $declaringClass): array { if (!empty($functionDef->returnTypeCheck)) { - return $functionDef->returnTypeCheck; - } - $type = $functionDef->returnType; - if ($type === Type::VAR) { - return [['kind' => 'isMixed']]; - } - if ($type === Type::OBJECT) { - return $functionDef->returnClass - ? [['kind' => 'instanceof', 'class' => $functionDef->returnClass]] - : [['kind' => 'isObject']]; + return array_map( + fn (array $type): array => $this->normalizeReturnTypeEntry($type, $declaringClass), + $functionDef->returnTypeCheck, + ); } - return match ($type) { - Type::INT => [['kind' => 'isInt']], - Type::FLOAT => [['kind' => 'isFloat']], - Type::BOOL => [['kind' => 'isBool']], - Type::STR => [['kind' => 'isString']], - Type::ARRAY => [['kind' => 'isArray']], - Type::RESOURCE => [['kind' => 'isResource']], - default => [['kind' => 'isMixed']], + + if ($functionDef->returnTypeKeyword === 'static') { + return [['kind' => 'isStatic', 'class' => $declaringClass]]; + } + if ($functionDef->returnType === Type::OBJECT && $functionDef->returnClass !== '') { + return [['kind' => 'instanceof', 'class' => $functionDef->returnClass]]; + } + + $declaredType = strtolower($functionDef->returnTypeStr); + return match ($declaredType) { + 'mixed' => [['kind' => 'isMixed']], + 'never' => [['kind' => 'isNever']], + 'void' => [['kind' => 'isVoid']], + 'null' => [['kind' => 'isNull']], + 'true' => [['kind' => 'isTrue']], + 'false' => [['kind' => 'isFalse']], + 'callable' => [['kind' => 'callable']], + 'iterable' => [['kind' => 'iterable']], + 'object' => [['kind' => 'isObject']], + default => match ($functionDef->returnType) { + Type::INT => [['kind' => 'isInt']], + Type::FLOAT => [['kind' => 'isFloat']], + Type::BOOL => [['kind' => 'isBool']], + Type::STR => [['kind' => 'isString']], + Type::ARRAY => [['kind' => 'isArray']], + Type::RESOURCE => [['kind' => 'isResource']], + Type::OBJECT => [['kind' => 'isObject']], + default => [['kind' => 'isMixed']], + }, }; } - private function isReturnTypeSubtype(array $childTypes, array $parentTypes): bool + private function normalizeReturnTypeEntry(array $type, string $declaringClass): array { - foreach ($childTypes as $childType) { - if (!$this->isReturnTypeCoveredBy($childType, $parentTypes)) { - return false; - } + if (($type['kind'] ?? null) === 'allOf') { + $type['types'] = array_map( + fn (array $member): array => $this->normalizeReturnTypeEntry($member, $declaringClass), + $type['types'], + ); + } elseif (($type['kind'] ?? null) === 'instanceof' && ($type['class'] ?? null) === 'static') { + $type = ['kind' => 'isStatic', 'class' => $declaringClass]; } - return true; + return $type; } private function isReturnTypeCoveredBy(array $childType, array $parentTypes): bool { - $childKind = $childType['kind'] ?? null; + $childClause = ($childType['kind'] ?? null) === 'allOf' + ? $childType['types'] + : [$childType]; - // Child is an intersection (A&B): it is a subtype only if every member - // is individually a subtype of the parent type. - if ($childKind === 'allOf') { - foreach ($childType['types'] as $member) { - if (!$this->isReturnTypeCoveredBy($member, $parentTypes)) { - return false; - } + foreach ($parentTypes as $parentType) { + $parentClause = ($parentType['kind'] ?? null) === 'allOf' + ? $parentType['types'] + : [$parentType]; + if ($this->isReturnTypeClauseSubtype($childClause, $parentClause)) { + return true; } - return true; } + return false; + } - foreach ($parentTypes as $parentType) { - $parentKind = $parentType['kind'] ?? null; - - // Parent is an intersection (A&B): the child must be a subtype of - // every member of the intersection. - if ($parentKind === 'allOf') { - $ok = true; - foreach ($parentType['types'] as $member) { - if (!$this->isReturnTypeCoveredBy($childType, [$member])) { - $ok = false; - break; - } - } - if ($ok) { - return true; + private function isReturnTypeClauseSubtype(array $childClause, array $parentClause): bool + { + foreach ($parentClause as $parentType) { + $covered = false; + foreach ($childClause as $childType) { + if ($this->isReturnTypeEntryCompatible($childType, $parentType)) { + $covered = true; + break; } - continue; } - - if ($this->isReturnTypeEntryCompatible($childKind, $childType, $parentKind, $parentType)) { - return true; + if (!$covered) { + return false; } } - - return false; + return true; } - private function isReturnTypeEntryCompatible( - ?string $childKind, - array $childType, - ?string $parentKind, - array $parentType - ): bool { - if ($childKind === 'isNull') { - // A null value is only compatible with a nullable (isNull) parent. - return $parentKind === 'isNull'; + private function isReturnTypeEntryCompatible(array $childType, array $parentType): bool + { + $childKind = $childType['kind'] ?? null; + $parentKind = $parentType['kind'] ?? null; + + if ($childKind === 'isNever' || $parentKind === 'isMixed') { + return true; } - if ($childKind === 'isObject') { - // Any object is compatible with a parent that accepts any object. - return $parentKind === 'isObject'; + if (($childKind === 'isTrue' || $childKind === 'isFalse') && $parentKind === 'isBool') { + return true; } - if ($childKind === 'isMixed') { - return $parentKind === 'isMixed'; + if ($childKind === 'isArray' && $parentKind === 'iterable') { + return true; + } + if ($childKind === 'isStatic') { + if ($parentKind === 'isObject' || $parentKind === 'isStatic') { + return true; + } + if ($parentKind === 'instanceof') { + return $this->isInheritedFrom( + $childType['class'] ?? '', + $parentType['class'] ?? '', + ); + } + return false; } if ($childKind === 'instanceof') { if ($parentKind === 'isObject') { return true; } + $childClass = $childType['class'] ?? ''; + if ($parentKind === 'iterable') { + return $childClass !== '' && $this->isInheritedFrom($childClass, 'Traversable'); + } if ($parentKind === 'instanceof') { - $childClass = $childType['class'] ?? ''; $parentClass = $parentType['class'] ?? ''; - if ($childClass === '' || $parentClass === '' || $childClass === 'static' || $parentClass === 'static') { - return false; - } - if ($childClass === $parentClass) { - return true; - } - return $this->isInheritedFrom($childClass, $parentClass); + return $childClass !== '' + && $parentClass !== '' + && $this->isInheritedFrom($childClass, $parentClass); } return false; } - // Scalar kinds must match exactly. - return $childKind === $parentKind; + return $childKind !== null && $childKind === $parentKind; } private function isParameterTypeOverrideCompatible(ArgInfo $childArg, ArgInfo $parentArg): bool diff --git a/tests/compiler/type_decl/return-type-covariance.phpt b/tests/compiler/type_decl/return-type-covariance.phpt index 3de3e6bb..68883ac3 100644 --- a/tests/compiler/type_decl/return-type-covariance.phpt +++ b/tests/compiler/type_decl/return-type-covariance.phpt @@ -34,6 +34,61 @@ class ObjectReturnImpl implements ObjectReturnContract } } +class StaticBase +{ + public function copy(): ?self + { + return $this; + } +} + +class StaticChild extends StaticBase +{ + public function copy(): ?static + { + return $this; + } +} + +interface IterableContract +{ + public function values(): iterable; +} + +class IterableImpl implements IterableContract +{ + public function values(): array + { + return [1, 2]; + } +} + +interface BoolContract +{ + public function enabled(): bool; +} + +class LiteralBoolImpl implements BoolContract +{ + public function enabled(): true + { + return true; + } +} + +abstract class VoidContract +{ + abstract public function stop(): void; +} + +abstract class NeverImpl extends VoidContract +{ + public function stop(): never + { + throw new RuntimeException('stop'); + } +} + function main() { $impl = new UnionReturnImpl(); @@ -43,9 +98,23 @@ function main() $built = $obj->build(); var_dump($built instanceof BaseType); var_dump($built instanceof ChildType); + + $static = new StaticChild(); + var_dump($static->copy() instanceof StaticChild); + + var_dump((new IterableImpl())->values()); + var_dump((new LiteralBoolImpl())->enabled()); } ?> --EXPECT-- int(42) bool(true) bool(true) +bool(true) +array(2) { + [0]=> + int(1) + [1]=> + int(2) +} +bool(true) From a43828446c1a7c2c30ad643ade9341fc5b8e181e Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 24 Jul 2026 17:42:29 +0800 Subject: [PATCH 13/16] fix(parser): preserve references across all array write paths --- src/Parser/AssignOpTrait.php | 31 ++++++++-------- tests/compiler/ref/array-ref-assign-002.phpt | 17 +++++++++ tests/compiler/ref/array-ref-assign-003.phpt | 37 ++++++++++++++++++++ 3 files changed, 70 insertions(+), 15 deletions(-) create mode 100644 tests/compiler/ref/array-ref-assign-003.phpt diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 5fdb32b4..745f9be2 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -42,24 +42,27 @@ trait AssignOpTrait $tmp = $this->genTmpVarName(); $this->addLocalVar($tmp, Type::VAR); - // 仅当目标是 php::Array 时使用 item/newItem: - // - item(dim, true) 直接返回元素 zval 地址,赋值时能穿透 IS_REFERENCE 写回, - // 修复 $arr = [&$x] / $arr[] = &$x 这类数组元素引用的写回问题; - // - 对于 ArrayAccess 对象(如 ArrayObject)或类型未知(VAR)的变量,item 不存在或语义不符, - // 必须继续使用 offsetSet(对象数组元素的引用写回由对象自身保证,编译器不负责)。 - $isPhpArray = $this->getVarType($array) === Type::ARRAY; + // item(dim, true) updates an existing reference's value, while offsetSet() + // replaces the array bucket and breaks the reference. Keep offsetSet() for + // ArrayAccess objects; dynamically typed/reference containers need a + // runtime array check because either representation is possible. + $arrayType = $this->getVarType($array); if ($left->dim === null) { - if ($isPhpArray) { - return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.newItem() = {$tmp}" . '), ' . $tmp . ')'; - } return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet(" . self::VALUE_NULL . ", {$tmp})" . '), ' . $tmp . ')'; } $dim = $this->parseIdentifier($left->dim); - if ($isPhpArray) { + if ($arrayType === Type::ARRAY) { return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.item({$dim}, true) = {$tmp}" . '), ' . $tmp . ')'; } + if ($arrayType === Type::VAR || $arrayType === Type::REF) { + $writeArray = "static_cast({$array}.item({$dim}, true) = {$tmp})"; + $writeOther = "{$array}.offsetSet({$dim}, {$tmp})"; + return $code . '((' . $tmp . ' = ' . $value . ', ' + . "({$array}.isArray() ? {$writeArray} : {$writeOther})" + . '), ' . $tmp . ')'; + } return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet({$dim}, {$tmp})" . '), ' . $tmp . ')'; } @@ -759,6 +762,9 @@ trait AssignOpTrait } $left = $this->parseWritableIdentifier($expr->var); + // Keep this write-context form for every RHS kind. Re-parsing it as a + // read later breaks append and missing-key targets such as + // `$array[] =& $source`. if ($this->isVarExpr($expr->var)) { if (!$this->hasVar($left)) { @@ -835,15 +841,10 @@ trait AssignOpTrait } } } elseif ($this->isPropertyFetch($expr->expr)) { - $left = $this->parseIdentifier($expr->var); $rightExpr = $tmpVar . ' = ' . $this->emitDynamicPropertyFetchRef($expr->expr, $expr); } elseif ($this->isStaticPropertyFetch($expr->expr)) { - $left = $this->parseIdentifier($expr->var); $rightExpr = $tmpVar . ' = ' . $this->emitStaticPropertyFetchRef($expr->expr, $expr); } elseif ($this->isArrayDimFetch($expr->expr)) { - // $left 已在函数开头通过 parseWritableIdentifier($expr->var) 正确计算, - // 这里不可再用 parseIdentifier() 覆盖,否则当左值是数组追加($arr[] = &$x) - // 或数组元素($arr[$k] = &$x)时会被当作读取而报错 "Cannot use [] for reading"。 $array = $this->parseWritableIdentifier($expr->expr->var); if ($expr->expr->dim == null) { $this->fatalError($expr, 'Cannot assign reference to array dim fetch without dim'); diff --git a/tests/compiler/ref/array-ref-assign-002.phpt b/tests/compiler/ref/array-ref-assign-002.phpt index 025e6bad..ac441ca0 100644 --- a/tests/compiler/ref/array-ref-assign-002.phpt +++ b/tests/compiler/ref/array-ref-assign-002.phpt @@ -2,6 +2,12 @@ array reference assignment to element: $arr[$k] = &$v writes back through reference --FILE-- value; + $propertyRefs[2] = &RefSource::$staticValue; + $propertyRefs[0] = 333; + $propertyRefs[2] = 444; + var_dump($source->value, RefSource::$staticValue); } ?> --EXPECT-- @@ -32,3 +47,5 @@ int(200) int(111) int(222) int(77) +int(333) +int(444) diff --git a/tests/compiler/ref/array-ref-assign-003.phpt b/tests/compiler/ref/array-ref-assign-003.phpt new file mode 100644 index 00000000..6e9f8ca4 --- /dev/null +++ b/tests/compiler/ref/array-ref-assign-003.phpt @@ -0,0 +1,37 @@ +--TEST-- +dynamically typed array element assignment preserves references and ArrayAccess writes +--FILE-- + +--EXPECT-- +int(123) +int(123) +int(234) +int(234) +int(456) From dfa10c8cbc9873b67d665ffbe86026a4c2415f1f Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 24 Jul 2026 17:59:19 +0800 Subject: [PATCH 14/16] fix(compiler): convert runtime constants in default helpers --- src/Generator/DefaultArgumentGenerator.php | 25 +++++++++- src/Preprocessor.php | 50 +------------------ .../class-const-default-value-typed.phpt | 19 ++++--- 3 files changed, 37 insertions(+), 57 deletions(-) diff --git a/src/Generator/DefaultArgumentGenerator.php b/src/Generator/DefaultArgumentGenerator.php index 891c4b77..ef41305a 100644 --- a/src/Generator/DefaultArgumentGenerator.php +++ b/src/Generator/DefaultArgumentGenerator.php @@ -88,7 +88,8 @@ trait DefaultArgumentGenerator $code .= 'return ' . $plan->expr . ';' . PHP_EOL; } } else { - $code .= 'return ' . $argInfo->default . ';' . PHP_EOL; + $default = $this->convertRuntimeConstantDefault($type, $argInfo->default); + $code .= 'return ' . $default . ';' . PHP_EOL; } $code .= '}' . PHP_EOL . PHP_EOL; @@ -98,6 +99,28 @@ trait DefaultArgumentGenerator return $code; } + /** + * Runtime constant lookup returns Variant, but a typed default helper must + * return its native C++ type explicitly. Convert the complete expression so + * constants nested in expressions are covered as well. + */ + private function convertRuntimeConstantDefault(string $type, string $default): string + { + if (!str_contains($default, 'php::constant(')) { + return $default; + } + + return match ($type) { + Type::INT => 'php::toInt(' . $default . ')', + Type::FLOAT => 'php::toFloat(' . $default . ')', + Type::BOOL => 'php::toBool(' . $default . ')', + Type::STR => 'php::toString(' . $default . ')', + Type::ARRAY => 'php::toArray(' . $default . ')', + Type::OBJECT => 'php::toObject(' . $default . ')', + default => $default, + }; + } + private function shouldGenerateDefaultArgumentHelper(ArgInfo $argInfo): bool { if ($argInfo->variadic) { diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 3e46799d..877cc4f2 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -83,53 +83,6 @@ class Preprocessor extends CompilerBase return $type . ' ' . $argInfo->name; } - /** - * A default value that can only be resolved at runtime (e.g. a class/global - * constant coming from a class that is not compiled into the binary) is emitted - * as a `php::constant(...)` call, which returns a `php::Variant`. - * - * Copy-initializing a typed (non-Variant) parameter such as `php::Int`, - * `php::Float`, `php::Bool`, `php::Str`, `php::Array` or `php::Object` from a - * `php::Variant` is rejected by C++ because the conversion is explicit: - * - * php::Int type = php::constant(...); // error C2440 - * - * The function body already converts such values with `php::toInt(...)` / - * `php::toFloat(...)` / ... (see convertExprFromType), so we wrap the default - * with the very same conversion here. This keeps the declaration consistent with - * the body and produces compilable code: - * - * php::Int type = php::toInt(php::constant(...)); // OK - * - * Parameters whose effective type is `php::Var` (including Stream/Box, which are - * mapped to `php::Var`) accept a `php::Variant` directly, so they are left alone. - */ - protected function wrapScalarDefaultValue(string $type, string $defaultExpr): string - { - if (!str_starts_with($defaultExpr, 'php::constant(')) { - return $defaultExpr; - } - $target = $type; - if ($target === Type::STREAM || $target === Type::BOX) { - $target = Type::VAR; - } - static $converters = [ - Type::INT => 'php::toInt', - Type::FLOAT => 'php::toFloat', - Type::BOOL => 'php::toBool', - Type::STR => 'php::toString', - Type::ARRAY => 'php::toArray', - Type::OBJECT => 'php::toObject', - Type::BIGINT => 'php::toBigInt', - Type::DECIMAL => 'php::toDecimal', - Type::BIGFLOAT => 'php::toBigFloat', - ]; - if (isset($converters[$target])) { - return $converters[$target] . '(' . $defaultExpr . ')'; - } - return $defaultExpr; - } - public function getCppFile(string $file): string { $info = pathinfo($file); @@ -515,8 +468,7 @@ class Preprocessor extends CompilerBase $argInfo->default = 'php::newReference(' . $this->parseParamDefaultValue($param->default) . ')'; } } else { - $defaultExpr = $arrayInitPlan ? $arrayInitPlan->expr : $this->parseParamDefaultValue($param->default); - $argInfo->default = $this->wrapScalarDefaultValue($argInfo->type, $defaultExpr); + $argInfo->default = $arrayInitPlan ? $arrayInitPlan->expr : $this->parseParamDefaultValue($param->default); $argInfo->arrayInitPlan = $arrayInitPlan; $argInfo->defaultValue = $param->default; } diff --git a/tests/compiler/const/class-const-default-value-typed.phpt b/tests/compiler/const/class-const-default-value-typed.phpt index 7ee526ed..d9b5d26d 100644 --- a/tests/compiler/const/class-const-default-value-typed.phpt +++ b/tests/compiler/const/class-const-default-value-typed.phpt @@ -5,14 +5,15 @@ Typed parameter default value from an unresolvable (external) class constant class TypedDefault { - // \ArrayObject is an internal (external) class whose constants cannot be folded - // at compile time, so the default is emitted as php::constant(...). The parameter - // type `int` maps to php::Int, which cannot be copy-initialized from the - // php::Variant returned by php::constant(...). The compiler must wrap the default - // in php::Int(...) so the generated C++ compiles. - public function run(int $value = \ArrayObject::ARRAY_AS_PROPS) + public function run( + int $value = \ArrayObject::ARRAY_AS_PROPS, + float $floatValue = \ArrayObject::ARRAY_AS_PROPS, + string $format = \DateTime::ATOM, + int $composite = 1 | \ArrayObject::ARRAY_AS_PROPS, + mixed $variant = \ArrayObject::STD_PROP_LIST, + ) { - var_dump($value); + var_dump($value, $floatValue, $format, $composite, $variant); } } @@ -23,3 +24,7 @@ function main() ?> --EXPECT-- int(2) +float(2) +string(13) "Y-m-d\TH:i:sP" +int(3) +int(1) From 3211be2833bcba4c97a41280edb0c5f17dd1a335 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 24 Jul 2026 18:22:18 +0800 Subject: [PATCH 15/16] fix(stub): harden heredoc and nowdoc C++ escaping --- src/gen_stub.php | 38 ++++++++----------- .../const/class-const-heredoc-nowdoc.phpt | 10 ++--- .../const/heredoc-nowdoc-const-defaults.phpt | 21 ++++++++-- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/gen_stub.php b/src/gen_stub.php index ff1406a0..9dcce8fb 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -834,7 +834,10 @@ class ArgInfo { private function getDefaultValueAsArginfoString(): string { if ($this->hasProperDefaultValue()) { - return '"' . addslashes($this->defaultValue) . '"'; + // The default value is a PHP expression embedded in a C string. + // Escape for the outer C layer only; addslashes() leaves line + // breaks and other control bytes untouched, producing invalid C++. + return '"' . getTranslator()->escapeString($this->defaultValue) . '"'; } return "NULL"; @@ -2485,7 +2488,9 @@ class EvaluatedValue if ($forStringDef === '') { $forStringDef = "{$zvalName}_str"; } - $code .= "\tzend_string *$forStringDef = zend_string_init($cExpr, strlen($cExpr), 1);\n"; + // getCExpr() emits a C string literal here. sizeof() preserves + // embedded NUL bytes, unlike strlen(). + $code .= "\tzend_string *$forStringDef = zend_string_init($cExpr, sizeof($cExpr) - 1, 1);\n"; $code .= "\tZVAL_STR(&$zvalName, $forStringDef);\n"; } } elseif ($this->type->isArray()) { @@ -2515,20 +2520,12 @@ class EvaluatedValue return '"' . getTranslator()->escapeString((string) $this->value) . '"'; } elseif ($this->expr instanceof Expr\ConstFetch) { return getTranslator()->getConstValue($this->expr->name->toString()); - } elseif ($this->expr instanceof String_) { - // Heredoc/nowdoc and quoted string literals: emit the decoded - // value directly. Pretty-printing a heredoc/nowdoc would leak - // the `<<escapeString((string) $this->value) . '"'; - } else { - // ConstExprEvaluator has already reduced concatenations and - // other constant string expressions to their PHP value. Emit - // that value as a C string literal instead of rejecting every - // non-literal string expression. - return '"' . getTranslator()->escapeString((string) $this->value) . '"'; } + + // ConstExprEvaluator has already decoded literal syntax and + // reduced constant string expressions. Emitting that value avoids + // leaking heredoc/nowdoc source syntax into generated C++. + return '"' . getTranslator()->escapeString((string) $this->value) . '"'; } elseif ($this->type->isInt() or $this->type->isFloat()) { return strval($this->value); } elseif ($this->type->isBool()) { @@ -5113,13 +5110,10 @@ function parseFunctionLike( if ($param->default instanceof Expr\ClassConstFetch && $param->default->class->toLowerString() === "self") { $defaultValue = getTranslator()->getClassConstValue($func, $name->className->name, $param->default->name->name); $defaultValue = var_export($defaultValue, true); - } elseif ($param->default instanceof String_ && - in_array($param->default->getAttribute('kind'), [String_::KIND_HEREDOC, String_::KIND_NOWDOC], true) - ) { - // heredoc/nowdoc: prettyPrint 会输出 `<<escapeString((string) $param->default->value) . '"'; + } elseif ($param->default instanceof String_) { + // Keep this as a PHP expression. ArgInfo escapes the expression + // separately when embedding it in generated C++. + $defaultValue = var_export($param->default->value, true); } else { $defaultValue = $param->default ? $prettyPrinter->prettyPrintExpr($param->default) : null; } diff --git a/tests/compiler/const/class-const-heredoc-nowdoc.phpt b/tests/compiler/const/class-const-heredoc-nowdoc.phpt index 350c1ad7..28c5d6a5 100644 --- a/tests/compiler/const/class-const-heredoc-nowdoc.phpt +++ b/tests/compiler/const/class-const-heredoc-nowdoc.phpt @@ -6,18 +6,18 @@ class constants with heredoc and nowdoc syntax class Test { const VALUE1 = << --EXPECT-- -string(3) "abc" -string(3) "def" +string(60) "71756f7465202220736c617368205c206e756c2000207461622009203f3f" +string(54) "2476616c7565203f3f202271756f74656422205c6e205c70617468" diff --git a/tests/compiler/const/heredoc-nowdoc-const-defaults.phpt b/tests/compiler/const/heredoc-nowdoc-const-defaults.phpt index 07868984..3dc5192b 100644 --- a/tests/compiler/const/heredoc-nowdoc-const-defaults.phpt +++ b/tests/compiler/const/heredoc-nowdoc-const-defaults.phpt @@ -17,8 +17,14 @@ class WithProp ABC; } -function with_default(string $x = <<p); - var_dump(with_default()); + var_dump(bin2hex(with_default()), bin2hex(with_binary_default())); + + $default = (new ReflectionFunction('with_default'))->getParameters()[0]->getDefaultValue(); + $binaryDefault = (new ReflectionFunction('with_binary_default'))->getParameters()[0]->getDefaultValue(); + var_dump(bin2hex($default), bin2hex($binaryDefault)); } ?> --EXPECT-- string(3) "abc" string(3) "def" string(3) "xyz" -string(3) "abc" +string(54) "2476616c7565203f3f202271756f74656422205c6e205c70617468" +string(6) "410042" +string(54) "2476616c7565203f3f202271756f74656422205c6e205c70617468" +string(6) "410042" From 1527a44854613b9527d33894889f97e5f5aae4f7 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 24 Jul 2026 18:31:23 +0800 Subject: [PATCH 16/16] fix(stub): normalize special class constant references --- src/gen_stub.php | 42 +++++++------- tests/compiler/self-class/003.phpt | 90 +++++++++++++++++++++--------- 2 files changed, 87 insertions(+), 45 deletions(-) diff --git a/src/gen_stub.php b/src/gen_stub.php index 6e3bee06..9224f5fb 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -50,6 +50,18 @@ function getClassConstFetchClassName(Expr\ClassConstFetch $expr): string return $className; } +function resolveClassConstFetchClassName(Expr\ClassConstFetch $expr, string $currentClass): string +{ + $className = getClassConstFetchClassName($expr); + if (strcasecmp($className, 'self') === 0 || strcasecmp($className, 'static') === 0) { + return $currentClass; + } + if (strcasecmp($className, 'parent') === 0) { + return getTranslator()->getParentClass($currentClass); + } + return $className; +} + /** * @return FileInfo[] */ @@ -2325,7 +2337,11 @@ class EvaluatedValue } if ($expr instanceof Expr\ClassConstFetch) { - $originatingConstName = new ClassConstName($expr->class, $expr->name->toString()); + $className = resolveClassConstFetchClassName($expr, ClassInfo::$currentClass); + $originatingConstName = new ClassConstName( + new Name(ltrim($className, '\\')), + $expr->name->toString() + ); } else { $originatingConstName = new ConstName($expr->name->getAttribute('namespacedName'), $expr->name->toString()); } @@ -2361,26 +2377,12 @@ class EvaluatedValue if (strcasecmp($constName, 'class') === 0) { // `::class` is a compile-time magic constant that resolves to the // fully qualified class name of the referenced class. - $className = getClassConstFetchClassName($expr); - if (strcasecmp($className, 'self') === 0 || strcasecmp($className, 'static') === 0) { - return ClassInfo::$currentClass; - } - if (strcasecmp($className, 'parent') === 0) { - return getTranslator()->getParentClass(ClassInfo::$currentClass); - } - return ltrim($className, '\\'); - } - $class = getClassConstFetchClassName($expr); - // Resolve the special class-name keywords to concrete classes. - // Previously `self` was passed as both the class and a - // `ClassName::` name prefix (yielding `B::B::A`), and `parent` / - // `static` were passed verbatim (yielding `parent::A`), so the - // constant lookup always failed. - if (strcasecmp($class, 'self') === 0 || strcasecmp($class, 'static') === 0) { - $class = ClassInfo::$currentClass; - } elseif (strcasecmp($class, 'parent') === 0) { - $class = getTranslator()->getParentClass(ClassInfo::$currentClass); + return ltrim( + resolveClassConstFetchClassName($expr, ClassInfo::$currentClass), + '\\' + ); } + $class = resolveClassConstFetchClassName($expr, ClassInfo::$currentClass); $fqcnName = ltrim($class, '\\') . "::" . $constName; if (isset($allConstInfos[$fqcnName])) { return $allConstInfos[$fqcnName]->getValue($allConstInfos)->value; diff --git a/tests/compiler/self-class/003.phpt b/tests/compiler/self-class/003.phpt index 86bd825e..40300468 100644 --- a/tests/compiler/self-class/003.phpt +++ b/tests/compiler/self-class/003.phpt @@ -1,41 +1,81 @@ --TEST-- -Class constant referenced via self:: / parent:: / ClassName:: as a property default value +Class constants referenced through self, parent, explicit names and runtime static --FILE-- a); - var_dump($test->local); - var_dump($test->b); - var_dump($test->c); +namespace { + function main() + { + $test = new StubConstRef\Child; + var_dump( + StubConstRef\Child::SELF_LOCAL, + StubConstRef\Child::SELF_INHERITED, + StubConstRef\Child::PARENT_VALUE + ); + var_dump( + $test->selfLocal, + $test->selfInherited, + $test->parentValue, + $test->explicitValue + ); + var_dump($test->defaults()); + var_dump((new StubConstRef\GrandChild)->runtimeStatic()); + } } ?> --EXPECT-- +int(10) int(1) +int(2) int(10) int(1) int(2) +int(1) +array(2) { + [0]=> + int(10) + [1]=> + int(2) +} +int(30)