diff --git a/docs/INCOMPATIBLE_PHP_FEATURES.md b/docs/INCOMPATIBLE_PHP_FEATURES.md index 08ed7a1c..78432544 100644 --- a/docs/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/INCOMPATIBLE_PHP_FEATURES.md @@ -35,8 +35,8 @@ - 闭包和箭头函数不支持引用参数。 - 引用赋值的右侧必须是编译器可直接定位的变量、数组元素或对象属性;不支持从调用结果或复杂静态属性表达式建立引用。 -- 动态调用、闭包调用等编译期无法确定参数签名的调用,不能自动转换引用参数;需要显式使用 `refval()`。 -- `refval()` 只接受变量、数组元素或对象属性。 +- 动态调用、闭包调用等编译期无法确定参数签名的调用,不能自动转换引用参数;需要显式使用 `refval()` 或等价关键词方法 `toRef()`。 +- `refval()` / `toRef()` 只接受变量、数组元素或对象属性。 - 带 unpack 且尾部追加 named arguments 的调用会退化为动态调用,不能使用 native call。 ## 对象模型 diff --git a/examples/type-elimination.php b/examples/type-elimination.php new file mode 100644 index 00000000..7f372574 --- /dev/null +++ b/examples/type-elimination.php @@ -0,0 +1,26 @@ +test(); + $test->foo(); +} \ No newline at end of file diff --git a/phpunit/code/external-library-subclass-param.php b/phpunit/code/external-library-subclass-param.php new file mode 100644 index 00000000..89b6b6bd --- /dev/null +++ b/phpunit/code/external-library-subclass-param.php @@ -0,0 +1,12 @@ + 0; +} diff --git a/phpunit/code/re-assign-parent-to-child-object.php b/phpunit/code/re-assign-parent-to-child-object.php new file mode 100644 index 00000000..5d9a44c2 --- /dev/null +++ b/phpunit/code/re-assign-parent-to-child-object.php @@ -0,0 +1,15 @@ +exec('Cannot re-assign typed object `$obj1` from `stdClass` to `ArrayObject`', 're-assign-2.php'); } - public function testStdContainerStaticClassMismatch() + public function testCannotAssignParentObjectToChildTypedObject() { $this->exec( - 'Cannot assign object of class `StdContainerStaticChild` to std container value of class `StdContainerStaticBase`', - 'std-container-static-class-mismatch.php' + 'Cannot re-assign typed object `$child` from `TypedObjectAssignChild` to `TypedObjectAssignBase`', + 're-assign-parent-to-child-object.php' ); } + public function testCannotAssignUnrelatedObjectToInterfaceDeclaredObject() + { + $this->exec( + 'Cannot re-assign typed object `$object` from `InterfaceDeclaredAssignContract` to `InterfaceDeclaredAssignOther`', + 'interface-declared-object-mismatch.php' + ); + } + + public function testCannotPassUnrelatedObjectToInterfaceParameter() + { + $this->exec( + 'Argument `object` must be an instance of `InterfaceParamMismatchContract`, `InterfaceParamMismatchOther` given', + 'interface-param-mismatch.php' + ); + } + + public function testCannotReturnUnrelatedObjectFromInterfaceReturn() + { + $this->exec( + 'The return type is `InterfaceReturnMismatchContract`, cannot return an instance of `InterfaceReturnMismatchOther`', + 'interface-return-mismatch.php' + ); + } + + public function testStdContainerAcceptsSubclassValue() + { + $this->compile('std-container-static-class-mismatch.php'); + } + // === Object value assigned to non-object variable (right side is New_ expr) === public function testObjectToInt() @@ -71,12 +100,14 @@ class AssignTest extends \BaseTest $this->exec("Cannot re-assign `\$obj` from `php::Array` to `php::Object`", 're-assign-array-to-obj.php'); } - public function testCannotAssignSubclassToTypedObjectProperty() + public function testCanAssignSubclassToTypedObjectProperty() { - $this->exec( - 'Cannot assign object of class `TypedObjectPropChild` to object property `prop` of class `TypedObjectPropBase`', - 'object-prop-subclass-mismatch.php' - ); + $this->compile('object-prop-subclass-mismatch.php'); + } + + public function testCanPassExternalLibrarySubclassToParentParameter() + { + $this->compile('external-library-subclass-param.php'); } // === Str / Array value assigned to non-object scalar variable === diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index 30b2450b..40ae077e 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -262,6 +262,18 @@ YAML, 'myproject.yml', 'nested/config'); ); } + public function testGetFilesAcceptsYamlExtension(): void + { + $projectFile = $this->createProjectFile(<<<'YAML' +sources: + - main.php +YAML, 'project.yaml'); + + $files = $this->compiler->getFiles($projectFile); + + $this->assertSame([realpath(dirname($projectFile) . '/main.php')], $files); + } + public function testParseProjectYamlSupportsCliStyleModeAndOutputAliases(): void { $projectFile = $this->createProjectFile(<<<'YAML' @@ -380,6 +392,199 @@ YAML); $this->assertNotContains(realpath($projectDir . '/skipped/nested.php'), $files); } + public function testParseProjectYamlSupportsConditionalSourcesByPhpVersion(): void + { + $futureVersion = PHP_VERSION_ID + 10000; + $projectFile = $this->createProjectFile(<<= 80000 + - path: php-id-reversed.php + if: 80000 <= PHP_VERSION_ID + - path: missing-future.php + if: PHP_VERSION_ID >= {$futureVersion} +YAML); + $projectDir = dirname($projectFile); + file_put_contents($projectDir . '/php-current.php', "invokeMethod('parseProjectYaml', $projectFile); + + $this->assertContains(realpath($projectDir . '/main.php'), $files); + $this->assertContains(realpath($projectDir . '/php-current.php'), $files); + $this->assertContains(realpath($projectDir . '/php-id-reversed.php'), $files); + $this->assertNotContains($projectDir . '/missing-future.php', $files); + } + + public function testParseProjectYamlConditionalSourceAllowsIfBeforePath(): void + { + $projectFile = $this->createProjectFile(<<<'YAML' +sources: + - if: PHP_VERSION_ID >= 80000 + path: if-before-path.php +YAML); + $projectDir = dirname($projectFile); + file_put_contents($projectDir . '/if-before-path.php', "invokeMethod('parseProjectYaml', $projectFile); + + $this->assertSame([realpath($projectDir . '/if-before-path.php')], $files); + } + + public function testParseProjectYamlSupportsCompositeConditionalSources(): void + { + $projectFile = $this->createProjectFile(<<<'YAML' +sources: + - path: composite.php + if: PHP_VERSION_ID >= 80000 && PHP_VERSION_ID < 90000 +YAML); + $projectDir = dirname($projectFile); + file_put_contents($projectDir . '/composite.php', "invokeMethod('parseProjectYaml', $projectFile); + + $this->assertSame([realpath($projectDir . '/composite.php')], $files); + } + + public function testParseProjectYamlSupportsPhpVersionStringConditionalSources(): void + { + $major = PHP_MAJOR_VERSION; + $nextMajor = PHP_MAJOR_VERSION + 1; + $projectFile = $this->createProjectFile(<<= "{$major}.0.0" + - path: php-version-reversed.php + if: '"{$major}.0.0" <= PHP_VERSION' + - path: missing-next-major.php + if: PHP_VERSION >= "{$nextMajor}.0.0" +YAML); + $projectDir = dirname($projectFile); + file_put_contents($projectDir . '/php-version-current.php', "invokeMethod('parseProjectYaml', $projectFile); + + $this->assertSame( + [ + realpath($projectDir . '/php-version-current.php'), + realpath($projectDir . '/php-version-reversed.php'), + ], + $files + ); + } + + public function testParseProjectYamlSupportsAllVersionCompareOperators(): void + { + $current = PHP_VERSION; + $projectFile = $this->createProjectFile(<< "0.0.0" + - path: op-id-alias.php + if: PHP_VERSION_ID GE 80000 +YAML); + $projectDir = dirname($projectFile); + foreach (['lt', 'le', 'gt', 'ge', 'eq', 'ne', 'symbol-eq', 'symbol-ne', 'id-alias'] as $name) { + file_put_contents($projectDir . '/op-' . $name . '.php', "invokeMethod('parseProjectYaml', $projectFile); + + foreach (['lt', 'le', 'gt', 'ge', 'eq', 'ne', 'symbol-eq', 'symbol-ne', 'id-alias'] as $name) { + $this->assertContains(realpath($projectDir . '/op-' . $name . '.php'), $files); + } + } + + public function testParseProjectYamlSupportsPhpOsFamilyConditionalSources(): void + { + $osFamily = PHP_OS_FAMILY; + $otherFamily = $osFamily === 'Windows' ? 'Linux' : 'Windows'; + $projectFile = $this->createProjectFile(<<= 80000 + - path: os-or.php + if: PHP_OS_FAMILY == "{$otherFamily}" || PHP_OS_FAMILY == "{$osFamily}" + - path: missing-os.php + if: PHP_OS_FAMILY == "{$otherFamily}" && PHP_OS_FAMILY != "{$osFamily}" +YAML); + $projectDir = dirname($projectFile); + foreach (['current', 'not-windows', 'reversed', 'composite', 'or'] as $name) { + file_put_contents($projectDir . '/os-' . $name . '.php', "invokeMethod('parseProjectYaml', $projectFile); + + foreach (['current', 'not-windows', 'reversed', 'composite', 'or'] as $name) { + $this->assertContains(realpath($projectDir . '/os-' . $name . '.php'), $files); + } + $this->assertNotContains($projectDir . '/missing-os.php', $files); + } + + public function testParseProjectYamlRejectsUnsupportedPhpOsFamilyOperator(): void + { + $projectFile = $this->createProjectFile(<<<'YAML' +sources: + - path: main.php + if: PHP_OS_FAMILY >= "Linux" +YAML); + + $this->expectException(\PhpAot\Php\Exception\TestError::class); + $this->expectExceptionMessage('Unsupported source condition'); + + $this->invokeMethod('parseProjectYaml', $projectFile); + } + + public function testParseProjectYamlRejectsBarePhpVersionCondition(): void + { + $projectFile = $this->createProjectFile(<<<'YAML' +sources: + - path: main.php + if: PHP_VERSION +YAML); + + $this->expectException(\PhpAot\Php\Exception\TestError::class); + $this->expectExceptionMessage('Unsupported source condition'); + + $this->invokeMethod('parseProjectYaml', $projectFile); + } + + public function testParseProjectYamlRejectsUnsafeConditionalSourceExpression(): void + { + $projectFile = $this->createProjectFile(<<<'YAML' +sources: + - path: main.php + if: PHP_VERSION_ID >= getenv("MIN_PHP") +YAML); + + $this->expectException(\PhpAot\Php\Exception\TestError::class); + $this->expectExceptionMessage('Unsupported source condition'); + + $this->invokeMethod('parseProjectYaml', $projectFile); + } + public function testCCompileCommandOptionsKeepCommonUserConfiguration(): void { $this->setPropertyValue('userIncludePaths', ['/user/include']); @@ -655,7 +860,7 @@ YAML); public function testGetNamespacedFuncNameWithUseFunction(): void { $this->setPropertyValue('useFunctions', [ - 'helper_func' => 'App\\Lib', + 'helper_func' => 'App\\Lib\\helper_func', ]); $this->assertEquals( 'App\\Lib\\helper_func', @@ -678,7 +883,7 @@ YAML); public function testGetNamespacedFuncNameNotInUseFunctions(): void { - $this->setPropertyValue('useFunctions', ['other' => 'Some\\Ns']); + $this->setPropertyValue('useFunctions', ['other' => 'Some\\Ns\\other']); $this->assertEquals( 'my_func', $this->compiler->getNamespacedFuncName('my_func') diff --git a/phpunit/src/Context/FunctionContextTest.php b/phpunit/src/Context/FunctionContextTest.php index 420cd85c..42044100 100644 --- a/phpunit/src/Context/FunctionContextTest.php +++ b/phpunit/src/Context/FunctionContextTest.php @@ -29,7 +29,6 @@ class FunctionContextTest extends TestCase $this->assertSame(0, $ctx->scopeLevel); $this->assertFalse($ctx->inLoop); $this->assertFalse($ctx->inClosure); - $this->assertFalse($ctx->inAssignExpr); } public function testEnterScopeIncrementsLevel(): void @@ -72,9 +71,6 @@ class FunctionContextTest extends TestCase $ctx->inClosure = true; $this->assertTrue($ctx->inClosure); - $ctx->inAssignExpr = true; - $this->assertTrue($ctx->inAssignExpr); - $ctx->tmpVarIndex = 5; $this->assertSame(5, $ctx->tmpVarIndex); } diff --git a/phpunit/src/LoopOptimizerTest.php b/phpunit/src/LoopOptimizerTest.php new file mode 100644 index 00000000..1e7242cf --- /dev/null +++ b/phpunit/src/LoopOptimizerTest.php @@ -0,0 +1,36 @@ +addFiles([$testFile]); + $compiler->prepareFile($testFile); + $compiler->convertFile($testFile); + + $this->addToAssertionCount(1); + return ROOT_PATH . '/build/phpunit/code/' . preg_replace('/\.php$/', '.cc', $file); + } + + public function testForBoundInternalConstantIsFolded(): void + { + try { + $outputFile = $this->compileToCpp('loop/internal-constant-for-bound.php'); + } catch (TestError $e) { + $this->fail($e->getMessage()); + } + + $code = file_get_contents($outputFile); + $this->assertStringNotContainsString('PHP_FD_SETSIZE', $code); + $this->assertStringNotContainsString('php::constant', $code); + $this->assertStringContainsString('1024L', $code); + } +} diff --git a/phpunit/src/NativePropertyTest.php b/phpunit/src/NativePropertyTest.php index 11c31544..e1c8763f 100644 --- a/phpunit/src/NativePropertyTest.php +++ b/phpunit/src/NativePropertyTest.php @@ -5,7 +5,7 @@ use PhpAot\Php\Exception\TestError; class NativePropertyTest extends \BaseTest { - private function compile(string $file): string + private function compileNativeProperty(string $file): string { global $translator; @@ -23,7 +23,7 @@ class NativePropertyTest extends \BaseTest public function testFindNativePropertyUsesFullClassNameAcrossBranches(): void { try { - $this->compile('native-property-full-name.php'); + $this->compileNativeProperty('native-property-full-name.php'); } catch (TestError $e) { $this->fail($e->getMessage()); } @@ -32,7 +32,7 @@ class NativePropertyTest extends \BaseTest public function testStaticStaticPropertyUsesDynamicCalledClassPath(): void { try { - $outputFile = $this->compile('native-property-full-name.php'); + $outputFile = $this->compileNativeProperty('native-property-full-name.php'); } catch (TestError $e) { $this->fail($e->getMessage()); } diff --git a/phpunit/src/PreprocessorTest.php b/phpunit/src/PreprocessorTest.php index 19f2ae39..471092a9 100644 --- a/phpunit/src/PreprocessorTest.php +++ b/phpunit/src/PreprocessorTest.php @@ -207,17 +207,20 @@ class PreprocessorTest extends TestCase public function testGetParentClassWithNamespace(): void { - $extends = new Node\Name('BaseController'); - $this->setProperty('namespace', 'App\\Controllers'); - $result = $this->invokeMethod('getParentClass', $extends); - $this->assertEquals('App\\Controllers\\BaseController', $result); + $this->setProperty('classExtends', [ + 'app\\controllers\\homecontroller' => 'app\\controllers\\basecontroller', + ]); + $result = $this->compiler->getParentClass('App\\Controllers\\HomeController'); + $this->assertEquals('app\\controllers\\basecontroller', $result); } public function testGetParentClassFullyQualified(): void { - $extends = new Node\Name\FullyQualified('App\\Entity\\Base'); - $result = $this->invokeMethod('getParentClass', $extends); - $this->assertEquals('App\\Entity\\Base', $result); + $this->setProperty('classExtends', [ + 'app\\entity\\user' => 'app\\entity\\base', + ]); + $result = $this->compiler->getParentClass('\\App\\Entity\\User'); + $this->assertEquals('app\\entity\\base', $result); } // ======================================================================== diff --git a/phpunit/src/UndefineTest.php b/phpunit/src/UndefineTest.php index 3924ec4b..0d53fa76 100644 --- a/phpunit/src/UndefineTest.php +++ b/phpunit/src/UndefineTest.php @@ -20,11 +20,11 @@ class UndefineTest extends \BaseTest public function testPropertyAccessOnUndefinedVar(): void { - $this->exec('The variable `$obj` is undefined', 'undefined-prop-access.php'); + $this->compile('undefined-prop-access.php'); } public function testMethodCallOnUndefinedVar(): void { $this->exec('The variable `$obj` is undefined', 'undefined-method-call.php'); } -} \ No newline at end of file +} diff --git a/phpunit/src/UniversalMethodCallTest.php b/phpunit/src/UniversalMethodCallTest.php index 68d3b20b..154e8436 100644 --- a/phpunit/src/UniversalMethodCallTest.php +++ b/phpunit/src/UniversalMethodCallTest.php @@ -19,7 +19,7 @@ class UniversalMethodCallTest extends \BaseTest public function testVoidMethodCall() { - $this->exec('Cannot call method on void', 'void-method-call.php'); + $this->compile('void-method-call.php'); } } diff --git a/src/Php/ArgInfo.php b/src/Php/ArgInfo.php index 03d528b3..71ba32c3 100644 --- a/src/Php/ArgInfo.php +++ b/src/Php/ArgInfo.php @@ -21,6 +21,13 @@ class ArgInfo public ?ArrayInitPlan $arrayInitPlan = null; public ?Expr $defaultValue = null; public string $class = ''; + + /** + * Object type declared in the PHP signature, including interfaces. + * Unlike $class, this is only an assignment/type-check constraint and must + * not be used for typed-object native-call dispatch. + */ + public string $declaredClass = ''; public bool $byRef = false; public bool $variadic = false; public bool $nullable = false; diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 7b1a8d07..e70e77c5 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -125,6 +125,8 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont 'toBigFloat' => self::TYPE_BIGFLOAT, 'toDecimal' => self::TYPE_DECIMAL, 'toObject' => self::TYPE_OBJECT, + 'toAny' => self::TYPE_VAR, + 'toRef' => self::TYPE_REF, ]; private const array STREAM_FUNCTIONS = [ @@ -529,6 +531,17 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont return $this->context->objects[$object] ?? 'stdClass'; } + protected function getDeclaredObjectType(string $object): string + { + if (isset($this->context->declaredObjects[$object])) { + return $this->context->declaredObjects[$object]; + } + if (isset($this->context->objects[$object]) || isset($this->context->stableObjects[$object])) { + return $this->getObjectType($object); + } + return ''; + } + public function parseExpr(NodeAbstract $expr): string { if ($expr->hasAttribute('replace')) { @@ -1775,6 +1788,66 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont return ''; } + protected function detectDeclaredClassOfExpr(NodeAbstract $expr): string + { + // 对象表达式有两类类型信息: + // 1. detectClassOfExpr() 返回“实际可推断的类”,例如 new Foo()、typed object 变量; + // 2. getDeclaredObjectType() 返回变量声明/首次赋值记录的 declared type,可能是接口或抽象类。 + // 参数和属性赋值检查需要先使用实际类;实际类不可知时才退回 declared type。 + $class = $this->detectClassOfExpr($expr); + if ($class !== '') { + return $class; + } + if ($this->isVarExpr($expr)) { + return $this->getDeclaredObjectType($this->parseVariable($expr)); + } + return ''; + } + + protected function isObjectClassStaticallyAssignableTo(string $class, string $expected): bool + { + // 这个函数只回答“编译器在静态阶段能否证明 $class is-a $expected”。 + // 这里禁止使用 class_exists()/interface_exists()/is_a() 去查询当前运行编译器的 PHP 进程: + // - 编译器进程已加载的 Composer/工具类,不等价于被编译项目运行时可用的类; + // - 自举编译时还会把编译器自身依赖的外部库误判为项目静态类; + // - AOT 的静态判断必须只依赖 hasClass()/hasInterface() 记录的项目类图,或明确的内置类/接口。 + // 如果类不属于这些集合,说明它是动态类/外部库类,不能在这里静态判定,应返回 false, + // 由调用处决定是延迟到运行时 php::toObject()/TypeCheck,还是因为确定 concrete mismatch 而 fatal。 + $class = ltrim($class, '\\'); + $expected = ltrim($expected, '\\'); + if (strcasecmp($class, $expected) === 0) { + return true; + } + + if (!$this->hasClass($class) + && !$this->hasInterface($class) + && !$this->isInternalClass($class) + && !$this->isInternalInterface($class) + ) { + return false; + } + + return $this->isInheritedFrom($class, $expected); + } + + protected function isKnownConcreteObjectExpr(NodeAbstract $expr, string $class): bool + { + // “已知 concrete object” 的要求比“表达式写着 new SomeClass”更严格: + // 只有 AOT 项目类图中的类或内置类,编译器才能在静态阶段确认其继承关系。 + // 外部库类即使出现在 new 表达式中,也不能用当前编译器进程的反射信息判定, + // 否则会把编译器/Composer 运行环境泄漏进被编译项目的类型系统。 + if ($class === '' || $this->isInterface($class) || $this->isAbstractClass($class)) { + return false; + } + if (!$this->hasClass($class) && !$this->isInternalClass($class)) { + return false; + } + if (!$this->isNewExpr($expr) || !$this->isNameExpr($expr->class)) { + return false; + } + return $this->parseIdentifier($expr->class) !== 'static'; + } + protected function resolveClassNameArg(NodeAbstract $arg): string { if ($this->isScalarString($arg)) { @@ -1829,27 +1902,25 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont $returnType = self::TYPE_VAR; } + $returnObjectCheckClass = ''; // 返回值的表达式是一个类的对象 - $objectClass = $this->detectClassOfExpr($v->expr); - $returnClass = $this->getReturnClass(); + $objectClass = $this->detectDeclaredClassOfExpr($v->expr); + $returnClass = $this->context->inClosure ? '' : $this->getReturnClass(); if ($returnClass) { - if (!$objectClass or $this->hasInterface($objectClass)) { - // TODO 返回值的类型无法确定,或者是一个接口,无法继承关系,需要插入动态类型检测代码 - } elseif (!$this->isInheritedFrom($objectClass, $returnClass)) { - $this->fatalError($v, 'The return type is `' . $returnClass . '`, cannot return an instance of `' . $objectClass . '`'); - } - // 把子类当做父类返回时,父类必须是抽象类或者接口 - // 仅原生类进行静态检查,若类不存在,说明该类是动态类,无法进行编译期验证 - if ($objectClass and $objectClass !== $returnClass - and $this->hasClass($returnClass) - and !$this->isAbstractClass($returnClass) - and !$this->hasInterface($returnClass) - and !$this->isInternalInterface($returnClass)) { - $this->fatalError($v, "When returning a subclass `$objectClass` instance as parent type, the parent class `$returnClass` must be abstract/interface"); + if ($objectClass === '') { + $returnObjectCheckClass = $returnClass; + } elseif (!$this->isObjectClassStaticallyAssignableTo($objectClass, $returnClass)) { + if ($this->isKnownConcreteObjectExpr($v->expr, $objectClass)) { + $this->fatalError($v, 'The return type is `' . $returnClass . '`, cannot return an instance of `' . $objectClass . '`'); + } + $returnObjectCheckClass = $returnClass; } } $exprCode = $this->convertExprType($expr, $returnType, $type); + if ($returnObjectCheckClass !== '') { + $exprCode = $this->convertObjectExpr($exprCode, $this->getClassEntryPtr($returnObjectCheckClass)); + } // Union/nullable return type: always use tmpVar for runtime check if ($this->shouldCheckClosureReturnType()) { [$code, $tmpVar] = $this->genClosureCheckedReturnAssignment($exprCode); @@ -1989,9 +2060,10 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont protected function addObject(string $name, string $class): void { - // 接口、抽象类、非原生类,无法作为 TypedObject 使用 - if (!$this->isInterface($class) and !$this->isAbstractClass($class) and - ($this->isNativeClass($class) or $this->isInternalClass($class))) { + // Interfaces have no concrete method body for native calls. Abstract classes may have concrete methods. + if ($this->isInterface($class)) { + $this->context->declaredObjects[$name] = $class; + } elseif ($this->isNativeClass($class) or $this->isInternalClass($class)) { $this->context->objects[$name] = $class; } } @@ -3716,7 +3788,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont } $namedArgs[$arg->name->name] = true; $byRef = $funcName && $this->isReferenceNamedArgument($funcName, $className, $arg->name->name); - $value = ($byRef || $this->isRefvalCall($arg->value)) + $value = ($byRef || $this->isRefvalCall($arg->value) || $this->isToRefCall($arg->value)) ? $this->parseReferenceCallArgValue($arg) : $this->parseCallArgValue($arg); if ($separateNamedArgs) { @@ -3784,26 +3856,20 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont $this->addPositionalCallArg($array . '.itemRef(' . $this->identifierToStr($arg->value->dim) . ')', $arrayArgsVar, $list_args); continue; } - } elseif ($this->isFuncCallExpr($arg->value)) { - if ($this->isNameExpr($arg->value->name) and $arg->value->name->toString() === 'refval') { - if (count($arg->value->args) !== 1) { - $this->fatalError($arg, 'The refval function only accepts one parameter'); - } - $inner = $arg->value->args[0]->value; - if ($this->isVarExpr($inner)) { - $name = $this->parseVariable($inner); - // 消除 refval() 函数调用,直接使用变量 - $arg->value = $inner; - $this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args); - continue; - } - $expr = $this->expandRefvalExpr($inner, $arg); - if ($expr !== null) { - $this->addPositionalCallArg($expr, $arrayArgsVar, $list_args); - continue; - } - $this->fatalError($arg, 'The refval function only accepts a variable, array element, or object property'); + } elseif ($this->isReferenceWrapperCall($arg->value)) { + $inner = $this->unwrapReferenceWrapperCall($arg->value, $arg); + if ($this->isVarExpr($inner)) { + $name = $this->parseVariable($inner); + $arg->value = $inner; + $this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args); + continue; + } + $expr = $this->expandRefvalExpr($inner, $arg); + if ($expr !== null) { + $this->addPositionalCallArg($expr, $arrayArgsVar, $list_args); + continue; } + $this->fatalError($arg, 'The refval function only accepts a variable, array element, or object property'); } else { if ($byRef) { if ($this->isScalar($arg->value)) { @@ -3934,11 +4000,8 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont protected function parseReferenceCallArgValue(Node\Arg $arg): string { - if ($this->isRefvalCall($arg->value)) { - if (count($arg->value->args) !== 1) { - $this->fatalError($arg, 'The refval function only accepts one parameter'); - } - $arg->value = $arg->value->args[0]->value; + if ($this->isReferenceWrapperCall($arg->value)) { + $arg->value = $this->unwrapReferenceWrapperCall($arg->value, $arg); } if ($this->isVarExpr($arg->value)) { @@ -3976,6 +4039,37 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont return '&' . $tmpRef; } + protected function isToRefCall(NodeAbstract $expr): bool + { + return $this->isMethodCall($expr) + && $this->isNamedMethod($expr->name) + && $expr->name->toString() === 'toRef'; + } + + protected function isReferenceWrapperCall(NodeAbstract $expr): bool + { + return $this->isRefvalCall($expr) || $this->isToRefCall($expr); + } + + protected function unwrapReferenceWrapperCall(NodeAbstract $expr, NodeAbstract $errorNode): NodeAbstract + { + if ($this->isRefvalCall($expr)) { + if (count($expr->args) !== 1) { + $this->fatalError($errorNode, 'The refval function only accepts one parameter'); + } + return $expr->args[0]->value; + } + + if ($this->isToRefCall($expr)) { + if (!empty($expr->args)) { + $this->fatalError($errorNode, 'The toRef method does not accept parameters'); + } + return $expr->var; + } + + $this->fatalError($errorNode, 'Expected a reference wrapper call'); + } + /** * 展开 refval() 调用中的数组元素或对象属性,返回对应的 C++ 引用表达式。 * 若为普通变量则返回 null,由调用方自行处理。 @@ -4654,6 +4748,9 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont if ($name === 'PHP_EOL') { return '"' . $this->escapeString(PHP_EOL) . '"'; } + if ($this->isInternalScalarConstant($name)) { + return $this->getInternalScalarConstantValue($name); + } if ($scalar) { return constant($expr->name); } @@ -4768,8 +4865,35 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont protected function isInheritedFrom(string $class, string $expected): bool { + // 继承关系判断的唯一入口。调用者不应直接使用 PHP 运行时反射函数判断普通项目类。 + // 对 AOT 已扫描到的项目类/接口,必须走 classDef/interfaceDef 中的 extends/implements 图; + // 对 PHP 内置类/接口,可以使用 Zend 运行时反射,因为这部分属于目标 PHP 运行时的固定能力; + // 对动态类返回 true 表示“静态阶段无法否定”,后续必须保留运行时检查兜底。 + $class = ltrim($class, '\\'); + $expected = ltrim($expected, '\\'); + if (strcasecmp($class, $expected) === 0) { + return true; + } + $internal = ($this->isInternalClass($expected) or $this->isInternalInterface($expected)); $isInterface = ($this->hasInterface($expected) or $this->isInternalInterface($expected)); + + if ($this->hasInterface($class)) { + if (!$isInterface) { + return false; + } + return $this->interfaceExtends($class, $expected); + } + + if ($this->isInternalClass($class) or $this->isInternalInterface($class)) { + // 只允许内置类型之间使用 Zend 的继承关系。这里不是查询任意用户类, + // 因此不会把编译器进程加载过的外部库类混入项目静态类型系统。 + if (!$internal) { + return false; + } + return is_subclass_of($class, $expected); + } + // 类不存在,说明这是一个动态类,跳过静态检查,需要运行时检查 if (!$this->hasClass($class)) { return true; @@ -4789,6 +4913,9 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont return true; } if (!$this->hasInterface($check)) { + if ($internal && $this->isInternalInterface($check) && is_subclass_of($check, $expected)) { + return true; + } continue; } $interfaceDef = $this->getInterface($check); @@ -4796,9 +4923,6 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont $stack[] = $parentIface; } } - if (is_subclass_of($iface, $expected)) { - return true; - } } } else { if (strcasecmp($class, $expected) === 0) { @@ -4817,23 +4941,48 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont return false; } $class = $classDef->extends; + if ($this->isInternalClass($class)) { + // 项目类可以继承内置类。进入内置父类链后,后续关系交给 Zend 判断; + // 但 expected 也必须是内置类/接口,否则不能跨到外部用户类命名空间做运行时反射。 + return $internal && is_subclass_of($class, $expected); + } $classDef = $this->getClass($class); } } + private function interfaceExtends(string $interface, string $expected): bool + { + // 接口继承需要单独处理,因为 interfaceDef 没有 classDef 的父类链。 + // 这里同样只遍历 AOT 已知接口图;遇到内置接口时,才允许使用 Zend 的 is_subclass_of()。 + $stack = [$interface]; + while ($stack) { + $check = array_pop($stack); + if (strcasecmp($check, $expected) === 0) { + return true; + } + if (!$this->hasInterface($check)) { + if ($this->isInternalInterface($check) && $this->isInternalInterface($expected) && is_subclass_of($check, $expected)) { + return true; + } + continue; + } + $interfaceDef = $this->getInterface($check); + foreach ($interfaceDef->extendsList ?: ($interfaceDef->extends ? [$interfaceDef->extends] : []) as $parentInterface) { + $stack[] = $parentInterface; + } + } + return false; + } + protected function getTypeConvertedArg(Node\Arg $arg, ArgInfo $argInfo): string { $type = $this->detectTypeOfExpr($arg->value); $this->assertExprCanBeUsedAsValue($arg->value, 'function argument'); if ($argInfo->byRef) { - if ($this->isRefvalCall($arg->value)) { - if (count($arg->value->args) !== 1) { - $this->fatalError($arg, 'The refval function only accepts one parameter'); - } - $inner = $arg->value->args[0]->value; + if ($this->isReferenceWrapperCall($arg->value)) { + $inner = $this->unwrapReferenceWrapperCall($arg->value, $arg); if ($this->isVarExpr($inner)) { - // 消除 refval() 函数调用,直接使用变量 $arg->value = $inner; } else { $expr = $this->expandRefvalExpr($inner, $arg); @@ -4866,17 +5015,25 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont } if ($argInfo->type === self::TYPE_OBJECT) { - if ($this->isVarExpr($arg->value)) { - $object = $this->parseVariable($arg->value); - if ($this->isTypedObject($object)) { - $class = $this->getObjectType($object); - if ($class and $argInfo->class and !$this->isInheritedFrom($class, $argInfo->class)) { + $declaredClass = $argInfo->declaredClass ?: $argInfo->class; + if ($declaredClass !== '') { + $class = $this->detectDeclaredClassOfExpr($arg->value); + if ($class !== '') { + // native call 是性能热点,若静态阶段已经证明实参 is-a 声明类型, + // 就不要再生成 php::toObject($expr, target_ce) 做重复运行时检查。 + // 如果无法证明,但右值是已知 concrete object,说明一定不兼容,直接编译期 fatal; + // 其他动态/外部库/any 场景保留 php::toObject() 作为运行时兜底。 + if ($this->isObjectClassStaticallyAssignableTo($class, $declaredClass)) { + return $type === self::TYPE_OBJECT ? $expr : $this->convertObjectExpr($expr); + } + if ($this->isKnownConcreteObjectExpr($arg->value, $class)) { $argName = $argInfo->phpName ?: $this->unescapeVarName($argInfo->name); - $this->fatalError($arg, "Argument `{$argName}` must be an instance of `{$argInfo->class}`, `{$class}` given"); + $this->fatalError($arg, "Argument `{$argName}` must be an instance of `{$declaredClass}`, `{$class}` given"); } } + return $this->convertObjectExpr($expr, $this->getClassEntryPtr($declaredClass)); } - return $this->convertObjectExpr($expr); + return $type === self::TYPE_OBJECT ? $expr : $this->convertObjectExpr($expr); } return $this->convertExprType($expr, $argInfo->type, $type); @@ -4978,6 +5135,8 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont } if ($def->class === '' or $this->isAbstractClass($def->class) or $this->isInterface($def->class) or !$this->hasClass($def->class)) { + // 属性 declared class 若是接口、抽象类或动态类,当前属性布局优化无法静态确认最终对象类型。 + // 不在这里 fatal;后续 wrapObjectPropertyAssignTypeCheck() 会在需要时插入运行时检查。 return; } @@ -4986,7 +5145,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont if ($rightClass === '') { return; } - if ($rightClass !== $def->class) { + if (!$this->isObjectClassStaticallyAssignableTo($rightClass, $def->class)) { $this->fatalError( $left, "Cannot assign object of class `{$rightClass}` to {$label} `{$propName}` of class `{$def->class}`" @@ -5483,6 +5642,41 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont return self::TYPE_VAR; } + protected function isInternalScalarConstant(string $name): bool + { + return $this->isInternalConstant($name) && is_scalar($this->internalConstants[$name]); + } + + protected function getInternalScalarConstantValue(string $name): string|int|float + { + $value = $this->internalConstants[$name]; + if (is_int($value)) { + if ($value === PHP_INT_MIN) { + return 'LONG_MIN'; + } + if ($value === PHP_INT_MAX) { + return 'LONG_MAX'; + } + return $value . 'L'; + } + if (is_float($value)) { + if (is_nan($value)) { + return self::VALUE_NAN; + } + if (is_infinite($value)) { + return $value > 0 ? self::VALUE_INF : '-' . self::VALUE_INF; + } + return $this->genCValue($value); + } + if (is_bool($value)) { + return $value ? 1 : 0; + } + if (is_string($value)) { + return $this->genCharPtr($value, true); + } + $this->error('Unsupported constant type: ' . gettype($value)); + } + protected function parseSwitch(Node\Stmt\Switch_ $v): string { $cond = $v->cond; @@ -6129,7 +6323,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont } $receiver = $this->parseExpr($expr->args[0]->value); $className = $this->resolveClassNameArg($expr->args[1]->value); - return 'php::toObject(' . $receiver . ', ' . $this->getClassEntryPtr($className) . ', true)'; + return 'php::toObject(' . $receiver . ', ' . $this->getClassEntryPtr($className) . ')'; } protected function genToObjectCall(Expr\MethodCall $expr, string $receiver): string @@ -6138,7 +6332,15 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont return 'php::toObject(' . $receiver . ')'; } $className = $this->resolveClassNameArg($expr->args[0]->value); - return 'php::toObject(' . $receiver . ', ' . $this->getClassEntryPtr($className) . ', true)'; + return 'php::toObject(' . $receiver . ', ' . $this->getClassEntryPtr($className) . ')'; + } + + protected function genToRefCall(Expr\MethodCall $expr): string + { + if (!empty($expr->args)) { + $this->fatalError($expr, 'The toRef method does not accept parameters'); + } + return $this->parseChainedExpr($expr->var, self::OP_REFVAL); } protected function parseMethodCall(Expr\MethodCall $expr): string @@ -6173,6 +6375,12 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont if ($methodName === 'toObject') { return $this->genToObjectCall($expr, $object); } + if ($methodName === 'toRef') { + return $this->genToRefCall($expr); + } + if ($methodName === 'toAny' && !empty($expr->args)) { + $this->fatalError($expr, 'The toAny method does not accept parameters'); + } return $this->genToConvertCall($object, $methodName, $receiverType); } // __ keyword extensions @@ -6516,7 +6724,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont return $this->getNativePropertyAccess($expr)?->getClassDef(); } - private function getNativePropertyAccess(NodeAbstract $expr): ?NativePropertyAccess + public function getNativePropertyAccess(NodeAbstract $expr): ?NativePropertyAccess { $access = $expr->getAttribute('nativePropertyAccess'); return $access instanceof NativePropertyAccess ? $access : null; diff --git a/src/Php/Context/FunctionContext.php b/src/Php/Context/FunctionContext.php index fb700706..b2e4ebc5 100644 --- a/src/Php/Context/FunctionContext.php +++ b/src/Php/Context/FunctionContext.php @@ -29,6 +29,13 @@ class FunctionContext */ public array $objects = []; + /** + * Declared object constraints that are not used for native-call dispatch. + * + * @var array + */ + public array $declaredObjects = []; + /** * @var array */ @@ -76,6 +83,7 @@ class FunctionContext $this->staticVars = []; $this->arguments = []; $this->objects = []; + $this->declaredObjects = []; $this->stdArrays = []; $this->stdContainers = []; $this->objectProps = []; @@ -106,10 +114,11 @@ class FunctionContext unset($this->scopeLayouts[$this->scopeLevel]); } - public function resetAnalysisTemporaries(array $localVars, int $tmpVarIndex): void + public function resetAnalysisTemporaries(array $localVars, int $tmpVarIndex, array $declaredObjects): void { $this->localVars = $localVars; $this->tmpVarIndex = $tmpVarIndex; + $this->declaredObjects = $declaredObjects; $this->beforeStmtLines = []; $this->afterStmtLines = []; $this->objectProps = []; diff --git a/src/Php/Parser/AssignOpTrait.php b/src/Php/Parser/AssignOpTrait.php index 4f7f2317..2f359686 100644 --- a/src/Php/Parser/AssignOpTrait.php +++ b/src/Php/Parser/AssignOpTrait.php @@ -147,6 +147,8 @@ trait AssignOpTrait $propertyWriteTarget = $this->preparePropertyWriteTarget($left); $type = $this->detectTypeOfExpr($right); $finalVarType = $this->getNormalAssignType($type); + $runtimeObjectAssignClass = ''; + $rightExprOverride = null; if ($type === self::TYPE_VOID) { $type = self::TYPE_VAR; } @@ -169,11 +171,16 @@ trait AssignOpTrait if (!$this->hasVar($var)) { $this->addLocalVar($var, self::TYPE_OBJECT); $this->addObject($var, $rightClass); - } elseif ($this->isTypedObject($var)) { - $leftClass = $this->getObjectType($var); - // 对象的类不一致,不能互相赋值,必须使用 toObject() 对齐类型 - // 注意这里必须使用绝对相等比较,即使存在继承关系,类的方法也可能不一致 - if ($leftClass !== $rightClass) { + } elseif (($leftClass = $this->getDeclaredObjectType($var)) !== '') { + if ($this->isObjectClassStaticallyAssignableTo($rightClass, $leftClass)) { + // A child object can be assigned to a parent typed object. + } elseif ($this->isInterface($rightClass) || $this->isAbstractClass($rightClass) || $this->isObjectClassStaticallyAssignableTo($leftClass, $rightClass)) { + if ($this->isKnownConcreteObjectExpr($right, $rightClass)) { + $this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$rightClass}`"); + } + // Parent/interface/abstract declarations are not precise enough for a concrete typed object. + $runtimeObjectAssignClass = $leftClass; + } else { $this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$rightClass}`"); } } else { @@ -199,8 +206,9 @@ trait AssignOpTrait if (!$this->hasVar($var)) { $this->addLocalVar($var, $type); $finalVarType = $type; + return $var . ' = ' . $this->parseIdentifier($right->args[0]->value); } - return $var . ' = ' . $this->parseIdentifier($right->args[0]->value); + $rightExprOverride = $this->parseIdentifier($right->args[0]->value); } else { $type = $type === self::TYPE_VOID ? self::TYPE_VAR : $type; } @@ -241,10 +249,16 @@ trait AssignOpTrait $rightVar = $this->parseIdentifier($right); $type = $this->isStdContainer($rightVar) ? self::TYPE_ARRAY : $this->getVarType($rightVar); $finalVarType = $this->getNormalAssignType($type); - if ($this->isTypedObject($rightVar) and $this->isTypedObject($var)) { - $leftClass = $this->getObjectType($var); - $rightClass = $this->getObjectType($rightVar); - $this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$rightClass}`"); + $leftClass = $this->getDeclaredObjectType($var); + $rightClass = $this->getDeclaredObjectType($rightVar); + if ($leftClass !== '' and $rightClass !== '') { + if ($this->isObjectClassStaticallyAssignableTo($rightClass, $leftClass)) { + // A child object can be assigned to a parent typed object. + } elseif ($this->isInterface($rightClass) || $this->isAbstractClass($rightClass) || $this->isObjectClassStaticallyAssignableTo($leftClass, $rightClass)) { + $runtimeObjectAssignClass = $leftClass; + } else { + $this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$rightClass}`"); + } } } // 变量第一次被赋值,确定其类型,由于 PHP 的变量作用域是 function 级的,在 for/while 块中声明的变量,可以在块外使用 @@ -255,6 +269,10 @@ trait AssignOpTrait } else { $finalVarType = $this->getVarType($var); $this->checkVarAssignExpr($left, $finalVarType, $type); + $declaredObjectClass = $this->getDeclaredObjectType($var); + if ($finalVarType === self::TYPE_OBJECT && $declaredObjectClass !== '' && ($type === self::TYPE_VAR || $type === self::TYPE_OBJECT)) { + $runtimeObjectAssignClass = $declaredObjectClass; + } } } } elseif ($this->isPropertyFetch($left) and !$this->isNativePropertyAccess($left)) { @@ -277,10 +295,13 @@ trait AssignOpTrait } $var = $this->parseWritableIdentifier($left); - $rightExpr = $this->parseAssignRightExpr($right); + $rightExpr = $rightExprOverride ?? $this->parseAssignRightExpr($right); if ($propertyWriteTarget !== null) { $rightExpr = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $right, $rightExpr); } + if ($runtimeObjectAssignClass !== '') { + $rightExpr = 'php::toObject(' . $rightExpr . ', ' . $this->getClassEntryPtr($runtimeObjectAssignClass) . ')'; + } $leftExprType = $this->detectTypeOfExpr($left); $rightExprType = $this->detectTypeOfExpr($right); if ($finalVarType === self::TYPE_VAR) { diff --git a/src/Php/Parser/StdContainerTrait.php b/src/Php/Parser/StdContainerTrait.php index 828cb642..f0c58c2f 100644 --- a/src/Php/Parser/StdContainerTrait.php +++ b/src/Php/Parser/StdContainerTrait.php @@ -541,12 +541,6 @@ trait StdContainerTrait } else { $class = $this->getNamespacedClassName($class); } - if ($this->hasInterface($class)) { - $this->fatalError($expr, "{$owner} class value cannot use interface `{$class}`"); - } - if ($this->isAbstractClass($class)) { - $this->fatalError($expr, "{$owner} class value cannot use abstract class `{$class}`"); - } return $class; } @@ -563,12 +557,12 @@ trait StdContainerTrait } $rightClass = $this->detectClassOfExpr($expr); if ($rightClass !== '') { - if ($rightClass !== $class) { + if (!$this->isObjectClassStaticallyAssignableTo($rightClass, $class)) { $this->fatalError($expr, "Cannot assign object of class `{$rightClass}` to std container value of class `{$class}`"); } } - return 'php::toObject(' . $valueExpr . ', ' . $this->getClassEntryPtr($class) . ', true)'; + return 'php::toObject(' . $valueExpr . ', ' . $this->getClassEntryPtr($class) . ')'; } protected function convertStdVarBackedExpr(string $targetType, string $valueExpr, NodeAbstract $expr): string diff --git a/src/Php/Preprocessor.php b/src/Php/Preprocessor.php index fbe71578..87600c62 100644 --- a/src/Php/Preprocessor.php +++ b/src/Php/Preprocessor.php @@ -266,7 +266,10 @@ class Preprocessor extends CompilerBase ) { $argInfo->explicitMixed = in_array(strtolower($this->parseIdentifier($param->type)), ['mixed', 'any'], true); } - if ($class and !$this->hasInterface($class) and !$this->isAbstractClass($class)) { + if ($class) { + $argInfo->declaredClass = $class; + } + if ($class and !$this->hasInterface($class)) { $argInfo->class = $class; } return $type; diff --git a/src/Php/Translator.php b/src/Php/Translator.php index 02fea3b4..eff272fa 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -594,7 +594,7 @@ class Translator extends Preprocessor $this->sourceDirs[] = $path; } else { $ext = pathinfo($path, PATHINFO_EXTENSION); - if ($ext === 'yml') { + if ($ext === 'yml' || $ext === 'yaml') { // YAML 配置模式:先解析 YAML $list = $this->parseProjectYaml($path); } elseif ($ext === 'php') { @@ -2031,7 +2031,11 @@ CODE; $this->error('`sources` must be array'); } $list = []; - foreach ($sources as $src) { + foreach ($sources as $sourceEntry) { + [$src, $condition] = $this->parseProjectYamlSourceEntry($sourceEntry); + if ($condition !== null && !$this->evaluateProjectYamlCondition($condition)) { + continue; + } $realPath = $this->getAbsolutePath($src, $projectDir); if (!$realPath) { $this->error('Source file not exists: `' . $src . '`'); @@ -2264,6 +2268,194 @@ CODE; return $this->filterIgnoredFiles($list); } + /** + * @return array{0: string, 1: string|null} + */ + protected function parseProjectYamlSourceEntry(mixed $entry): array + { + if (is_string($entry)) { + return [$entry, null]; + } + if (!is_array($entry)) { + $this->error('Each `sources` entry must be a string or map'); + } + + $path = $entry['path'] ?? $entry['source'] ?? $entry['file'] ?? null; + if (!is_string($path) || trim($path) === '') { + $this->error('Conditional `sources` entries must include a non-empty `path`'); + } + + $condition = $entry['if'] ?? $entry['when'] ?? null; + if ($condition !== null && !is_string($condition)) { + $this->error('Source condition must be a string'); + } + + return [$path, $condition]; + } + + protected function evaluateProjectYamlCondition(string $condition): bool + { + $condition = trim($condition); + if ($condition === '') { + $this->error('Source condition must not be empty'); + } + + $expr = $this->replaceProjectYamlPhpVersionComparisons($condition); + $expr = $this->replaceProjectYamlPhpOsFamilyComparisons($expr, $condition); + if (preg_match('/[A-Za-z_]/', $expr)) { + $this->error('Unsupported source condition: `' . $condition . '`'); + } + if (!preg_match('/^[0-9\s<>=!&|().+-]+$/', $expr)) { + $this->error('Unsupported source condition: `' . $condition . '`'); + } + if (preg_match('/(?error('Unsupported source condition: `' . $condition . '`'); + } + + try { + /** @phpstan-ignore-next-line */ + return (bool) eval('return (' . $expr . ');'); + } catch (\ParseError|\Throwable) { + $this->error('Invalid source condition: `' . $condition . '`'); + } + } + + protected function replaceProjectYamlPhpVersionComparisons(string $condition): string + { + $versionLiteral = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\''; + $operator = '(>=|<=|==|!=|<>|=|>|<|lt|le|gt|ge|eq|ne)'; + + $expr = preg_replace_callback( + '/\bPHP_VERSION_ID\b\s*' . $operator . '\s*([0-9]+)/i', + function (array $matches): string { + return version_compare(PHP_VERSION, $this->phpVersionIdToString((int) $matches[2]), $this->normalizeProjectYamlVersionOperator($matches[1])) ? '1' : '0'; + }, + $condition + ); + if ($expr === null) { + $this->error('Invalid source condition: `' . $condition . '`'); + } + + $expr = preg_replace_callback( + '/([0-9]+)\s*' . $operator . '\s*\bPHP_VERSION_ID\b/i', + function (array $matches): string { + return version_compare($this->phpVersionIdToString((int) $matches[1]), PHP_VERSION, $this->normalizeProjectYamlVersionOperator($matches[2])) ? '1' : '0'; + }, + $expr + ); + if ($expr === null) { + $this->error('Invalid source condition: `' . $condition . '`'); + } + + $expr = preg_replace_callback( + '/\bPHP_VERSION\b\s*' . $operator . '\s*(' . $versionLiteral . ')/i', + function (array $matches): string { + $version = stripcslashes(($matches[3] ?? '') !== '' ? $matches[3] : $matches[4]); + $this->assertProjectYamlVersionLiteral($version); + return version_compare(PHP_VERSION, $version, $this->normalizeProjectYamlVersionOperator($matches[1])) ? '1' : '0'; + }, + $expr + ); + if ($expr === null) { + $this->error('Invalid source condition: `' . $condition . '`'); + } + + $expr = preg_replace_callback( + '/(' . $versionLiteral . ')\s*' . $operator . '\s*\bPHP_VERSION\b/i', + function (array $matches): string { + $version = stripcslashes($matches[2] !== '' ? $matches[2] : $matches[3]); + $this->assertProjectYamlVersionLiteral($version); + return version_compare($version, PHP_VERSION, $this->normalizeProjectYamlVersionOperator($matches[4])) ? '1' : '0'; + }, + $expr + ); + if ($expr === null) { + $this->error('Invalid source condition: `' . $condition . '`'); + } + + if (preg_match('/\bPHP_VERSION(?:_ID)?\b/', $expr)) { + $this->error('Unsupported source condition: `' . $condition . '`'); + } + + return $expr; + } + + protected function replaceProjectYamlPhpOsFamilyComparisons(string $expr, string $condition): string + { + $stringLiteral = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\''; + $operator = '(==|!=)'; + + $expr = preg_replace_callback( + '/\bPHP_OS_FAMILY\b\s*' . $operator . '\s*(' . $stringLiteral . ')/i', + function (array $matches): string { + $expected = stripcslashes(($matches[3] ?? '') !== '' ? $matches[3] : $matches[4]); + $this->assertProjectYamlOsFamilyLiteral($expected); + $result = PHP_OS_FAMILY === $expected; + if ($matches[1] === '!=') { + $result = !$result; + } + return $result ? '1' : '0'; + }, + $expr + ); + if ($expr === null) { + $this->error('Invalid source condition: `' . $condition . '`'); + } + + $expr = preg_replace_callback( + '/(' . $stringLiteral . ')\s*' . $operator . '\s*\bPHP_OS_FAMILY\b/i', + function (array $matches): string { + $expected = stripcslashes(($matches[2] ?? '') !== '' ? $matches[2] : $matches[3]); + $this->assertProjectYamlOsFamilyLiteral($expected); + $result = $expected === PHP_OS_FAMILY; + if ($matches[4] === '!=') { + $result = !$result; + } + return $result ? '1' : '0'; + }, + $expr + ); + if ($expr === null) { + $this->error('Invalid source condition: `' . $condition . '`'); + } + + if (preg_match('/\bPHP_OS_FAMILY\b/', $expr)) { + $this->error('Unsupported source condition: `' . $condition . '`'); + } + + return $expr; + } + + protected function normalizeProjectYamlVersionOperator(string $operator): string + { + return strtolower($operator); + } + + protected function assertProjectYamlVersionLiteral(string $version): void + { + if ($version === '' || !preg_match('/^[0-9A-Za-z_.+\-]+$/', $version)) { + $this->error('Invalid PHP_VERSION literal: `' . $version . '`'); + } + } + + protected function assertProjectYamlOsFamilyLiteral(string $osFamily): void + { + if (!in_array($osFamily, ['Windows', 'BSD', 'Darwin', 'Solaris', 'Linux', 'Unknown'], true)) { + $this->error('Invalid PHP_OS_FAMILY literal: `' . $osFamily . '`'); + } + } + + protected function phpVersionIdToString(int $versionId): string + { + if ($versionId < 0) { + $this->error('Invalid PHP_VERSION_ID literal: `' . $versionId . '`'); + } + $major = intdiv($versionId, 10000); + $minor = intdiv($versionId % 10000, 100); + $patch = $versionId % 100; + return $major . '.' . $minor . '.' . $patch; + } + protected function getInternalCeInfo(string $ce): array { return [ @@ -2920,7 +3112,12 @@ CODE; } } $cppType = $this->getDefaultArgumentType($argInfo); - $expr = $this->convertExprFromType($argInfo->type, $argExpr); + $declaredClass = $argInfo->declaredClass ?: $argInfo->class; + if ($argInfo->type === self::TYPE_OBJECT && $declaredClass !== '') { + $expr = $this->convertObjectExpr($argExpr, $this->getClassEntryPtr($declaredClass)); + } else { + $expr = $this->convertExprFromType($argInfo->type, $argExpr); + } $cppCode .= $this->getIndent() . $cppType . ' ' . $var . ' = ' . $expr . ';' . PHP_EOL; } $callParams .= 'arg_' . $argInfo->name . ','; @@ -3110,8 +3307,8 @@ CODE; } foreach ($this->functionDef->argInfoList as $argInfo) { $this->addArgument($argInfo->name, $argInfo->variadic ? self::TYPE_ARRAY : $argInfo->type); - if (!$argInfo->variadic and $argInfo->class) { - $this->addObject($argInfo->name, $argInfo->class); + if (!$argInfo->variadic and $argInfo->declaredClass) { + $this->addObject($argInfo->name, $argInfo->declaredClass); } } @@ -3119,6 +3316,7 @@ CODE; if ($v->stmts) { $oriLocalVars = $this->context->localVars; $oriTmpVarIndex = $this->context->tmpVarIndex; + $oriDeclaredObjects = $this->context->declaredObjects; /** SSA/e-SSA analysis for the current function. Built once per function, discarded with the context. */ $ssaBuilder = new SsaBuilder($v->stmts, $this->functionDef->argInfoList); $ssaBuilder->build(); @@ -3131,7 +3329,7 @@ CODE; $this->optimizeLoopVars($ssaBuilder); $this->optimizeObjectProps($ssaBuilder); } - $this->context->resetAnalysisTemporaries($oriLocalVars, $oriTmpVarIndex); + $this->context->resetAnalysisTemporaries($oriLocalVars, $oriTmpVarIndex, $oriDeclaredObjects); } $stmts = ''; diff --git a/src/Php/UniversalMethodCall.php b/src/Php/UniversalMethodCall.php index cbe8bf91..fad51823 100644 --- a/src/Php/UniversalMethodCall.php +++ b/src/Php/UniversalMethodCall.php @@ -374,6 +374,7 @@ trait UniversalMethodCall 'toBigInt' => 'php::BigInt::newInstance(' . $receiver . ')', 'toBigFloat' => 'php::BigFloat::newInstance(' . $receiver . ')', 'toDecimal' => 'php::Decimal::newInstance(' . $receiver . ')', + 'toAny' => $receiver, default => $receiver, }; } diff --git a/tests/aot/class/abstract-return-typed-object.phpt b/tests/aot/class/abstract-return-typed-object.phpt new file mode 100644 index 00000000..c202f153 --- /dev/null +++ b/tests/aot/class/abstract-return-typed-object.phpt @@ -0,0 +1,38 @@ +--TEST-- +abstract class return type can be used as typed object +--FILE-- +name(); + } + + abstract public function name(): string; +} + +class AbstractTypedObjectImpl extends AbstractTypedObjectBase +{ + public function name(): string + { + return 'impl'; + } +} + +function makeAbstractTypedObject(): AbstractTypedObjectBase +{ + return new AbstractTypedObjectImpl(); +} + +function main(): void +{ + $object = makeAbstractTypedObject(); + var_dump($object->concreteName()); + var_dump($object->name()); +} +?> +--EXPECT-- +string(9) "base:impl" +string(4) "impl" diff --git a/tests/aot/class/interface-declared-object-assign.phpt b/tests/aot/class/interface-declared-object-assign.phpt new file mode 100644 index 00000000..82dedd88 --- /dev/null +++ b/tests/aot/class/interface-declared-object-assign.phpt @@ -0,0 +1,60 @@ +--TEST-- +interface declared object constrains assignment without native call typing +--FILE-- +name()); + + $object = new DeclaredObjectImpl(); + var_dump($object->name()); + + $object = nextDeclaredObject($object); + var_dump($object->name()); + + try { + $object = any(new DeclaredObjectOther()); + } catch (Throwable $e) { + echo $e->getMessage(), "\n"; + } + var_dump($object->name()); +} + +function main(): void +{ + testDeclaredObject(new DeclaredObjectImpl()); +} +?> +--EXPECT-- +string(4) "impl" +string(4) "impl" +string(4) "impl" +The parameter `object` must be instance of class `DeclaredObjectContract`, object of `DeclaredObjectOther` given +string(4) "impl" diff --git a/tests/aot/class/interface-dynamic-call-zend-check.phpt b/tests/aot/class/interface-dynamic-call-zend-check.phpt new file mode 100644 index 00000000..fa9837d4 --- /dev/null +++ b/tests/aot/class/interface-dynamic-call-zend-check.phpt @@ -0,0 +1,48 @@ +--TEST-- +dynamic call uses wrapper interface parameter type check +--ENV-- +USE_ZEND_ALLOC=0 +--FILE-- +name(); +} + +function callDynamic($callback, $object): void +{ + try { + var_dump($callback($object)); + } catch (Throwable $e) { + echo $e->getMessage(), "\n"; + } +} + +function main(): void +{ + $callback = 'acceptInterfaceDynamicCallCheck'; + callDynamic($callback, new InterfaceDynamicCallCheckImpl()); + callDynamic($callback, new InterfaceDynamicCallCheckOther()); +} +?> +--EXPECT-- +string(4) "impl" +The parameter `object` must be instance of class `InterfaceDynamicCallCheckContract`, object of `InterfaceDynamicCallCheckOther` given diff --git a/tests/aot/class/interface-native-call-toobject-opt.phpt b/tests/aot/class/interface-native-call-toobject-opt.phpt new file mode 100644 index 00000000..286884bd --- /dev/null +++ b/tests/aot/class/interface-native-call-toobject-opt.phpt @@ -0,0 +1,43 @@ +--TEST-- +interface parameter native call avoids redundant object check when statically safe +--FILE-- +name(); +} + +function main(): void +{ + $impl = new InterfaceNativeCallOptImpl(); + var_dump(acceptInterfaceNativeCallOpt($impl)); + + try { + $other = new InterfaceNativeCallOptOther(); + var_dump(acceptInterfaceNativeCallOpt($other->toAny())); + } catch (Throwable $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--EXPECT-- +string(4) "impl" +The parameter `object` must be instance of class `InterfaceNativeCallOptContract`, object of `InterfaceNativeCallOptOther` given diff --git a/tests/aot/class/interface-param-return-check.phpt b/tests/aot/class/interface-param-return-check.phpt new file mode 100644 index 00000000..d42de015 --- /dev/null +++ b/tests/aot/class/interface-param-return-check.phpt @@ -0,0 +1,61 @@ +--TEST-- +interface parameter and return type checks with dynamic object values +--FILE-- +name(); +} + +function returnInterfaceCallCheck($object): InterfaceCallCheckContract +{ + return $object->toAny(); +} + +function main(): void +{ + $impl = new InterfaceCallCheckImpl(); + $other = new InterfaceCallCheckOther(); + + var_dump(useInterfaceCallCheck($impl->toAny())); + try { + var_dump(useInterfaceCallCheck($other->toAny())); + } catch (Throwable $e) { + echo $e->getMessage(), "\n"; + } + + var_dump(returnInterfaceCallCheck(new InterfaceCallCheckImpl())->name()); + try { + var_dump(returnInterfaceCallCheck(new InterfaceCallCheckOther())->name()); + } catch (Throwable $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--EXPECT-- +string(4) "impl" +The parameter `object` must be instance of class `InterfaceCallCheckContract`, object of `InterfaceCallCheckOther` given +string(4) "impl" +The parameter `object` must be instance of class `InterfaceCallCheckContract`, object of `InterfaceCallCheckOther` given diff --git a/tests/aot/class/objval-parent.phpt b/tests/aot/class/objval-parent.phpt index ceeb8a02..f10104a1 100644 --- a/tests/aot/class/objval-parent.phpt +++ b/tests/aot/class/objval-parent.phpt @@ -17,6 +17,10 @@ class Child extends Base { public function castToParent($obj): Base { return objval($obj, parent::class); } + + public function toParent($obj): Base { + return $obj->toObject(parent::class); + } } function main() { @@ -25,8 +29,16 @@ function main() { $result = $c->castToParent($b); var_dump($result->name()); + + $result = $c->castToParent($c); + var_dump($result->name()); + + $result = $c->toParent($c); + var_dump($result->name()); } ?> --EXPECT-- string(4) "Base" +string(5) "Child" +string(5) "Child" diff --git a/tests/aot/class/typed-object-assign-any.phpt b/tests/aot/class/typed-object-assign-any.phpt new file mode 100644 index 00000000..2d4d02ea --- /dev/null +++ b/tests/aot/class/typed-object-assign-any.phpt @@ -0,0 +1,129 @@ +--TEST-- +typed object assignment from any uses runtime class check +--FILE-- +name()); + + $child = new AssignAnyChild(); + try { + $child = any(new AssignAnyBase()); + } catch (Throwable $e) { + echo $e->getMessage(), "\n"; + } + var_dump($child->name()); + + $child = new AssignAnyChild(); + $factory = new AssignAnyBaseFactory(); + $child = $factory->child(); + var_dump($child->name()); + try { + $child = $factory->base(); + } catch (Throwable $e) { + echo $e->getMessage(), "\n"; + } + var_dump($child->name()); + + $base = new AssignAnyBase(); + $child = new AssignAnyChild(); + $base = $child; + var_dump($base->name()); + + $child = new AssignAnyChild(); + $base = new AssignAnyBase(); + $base = $child; + $child = $base; + var_dump($child->name()); + + $child = new AssignAnyChild(); + $base = new AssignAnyBase(); + try { + $child = $base; + } catch (Throwable $e) { + echo $e->getMessage(), "\n"; + } + var_dump($child->name()); + + $impl = new AssignAnyImpl(); + $impl = $impl->next(); + var_dump($impl->ok()); + + $impl = new AssignAnyImpl(); + $other = new AssignAnyOther(); + try { + $impl = $other->next(); + } catch (Throwable $e) { + echo $e->getMessage(), "\n"; + } + var_dump($impl->ok()); +} +?> +--EXPECT-- +string(4) "base" +The parameter `object` must be instance of class `AssignAnyChild`, object of `AssignAnyBase` given +string(4) "base" +string(4) "base" +The parameter `object` must be instance of class `AssignAnyChild`, object of `AssignAnyBase` given +string(4) "base" +string(4) "base" +string(4) "base" +The parameter `object` must be instance of class `AssignAnyChild`, object of `AssignAnyBase` given +string(4) "base" +string(4) "impl" +The parameter `object` must be instance of class `AssignAnyImpl`, object of `AssignAnyOther` given +string(4) "impl" diff --git a/tests/aot/keyword_method/to-any-to-ref.phpt b/tests/aot/keyword_method/to-any-to-ref.phpt new file mode 100644 index 00000000..c9f617ce --- /dev/null +++ b/tests/aot/keyword_method/to-any-to-ref.phpt @@ -0,0 +1,57 @@ +--TEST-- +AOT keyword methods toAny() and toRef() +--FILE-- +toAny() / $b->toAny()); + + $name = 'php '; + append_text($name->toRef(), 'keyword'); + var_dump($name); + + $arr = ['key' => 'original']; + set_value($arr['key']->toRef(), 'array'); + var_dump($arr['key']); + + $obj = new stdClass(); + $obj->prop = 'object'; + append_text($obj->prop->toRef(), ' property'); + var_dump($obj->prop); + + $fn = 'set_value'; + $dynamic = 'old'; + $fn($dynamic->toRef(), 'dynamic'); + var_dump($dynamic); + + $named = 'old'; + $fn = 'set_named'; + $fn(label: 'named', value: $named->toRef()); + var_dump($named); +} +?> +--EXPECT-- +float(2.5) +string(11) "php keyword" +string(5) "array" +string(15) "object property" +string(7) "dynamic" +string(5) "named" diff --git a/tests/aot/loop/for-internal-constant-bound.phpt b/tests/aot/loop/for-internal-constant-bound.phpt new file mode 100644 index 00000000..8ebb162f --- /dev/null +++ b/tests/aot/loop/for-internal-constant-bound.phpt @@ -0,0 +1,12 @@ +--TEST-- +for loop with internal constant bound +--FILE-- + +--EXPECT-- +int(512) diff --git a/tests/aot/ref/reuse.phpt b/tests/aot/ref/reuse.phpt new file mode 100644 index 00000000..64aefeff --- /dev/null +++ b/tests/aot/ref/reuse.phpt @@ -0,0 +1,31 @@ +--TEST-- +object link operator +--FILE-- + +--EXPECT-- +int(1) +int(2) +int(3) +array(3) { + [0]=> + int(5) + [1]=> + int(6) + [2]=> + int(7) +} \ No newline at end of file diff --git a/tests/aot/std-array/006.phpt b/tests/aot/std-array/006.phpt index 50f22ba8..8a174c3b 100644 --- a/tests/aot/std-array/006.phpt +++ b/tests/aot/std-array/006.phpt @@ -1,5 +1,5 @@ --TEST-- -std array: exact class value type +std array: class value type accepts subclasses --FILE-- getValue()); } catch (Throwable $e) { echo $e->getMessage(), "\n"; } @@ -41,4 +42,4 @@ function main() { --EXPECT-- int(1) int(2) -The parameter `object` must be instance of class `StdArrayClassValue`, object of `StdArrayClassValueChild` given +int(3) diff --git a/tests/aot/std-map/004.phpt b/tests/aot/std-map/004.phpt index 5b2bf20c..1b8ed683 100644 --- a/tests/aot/std-map/004.phpt +++ b/tests/aot/std-map/004.phpt @@ -1,5 +1,5 @@ --TEST-- -std map: exact class value type +std map: class value type accepts subclasses --FILE-- getValue()); } catch (Throwable $e) { echo $e->getMessage(), "\n"; } @@ -41,4 +42,4 @@ function main() { --EXPECT-- int(1) int(2) -The parameter `object` must be instance of class `StdMapClassValue`, object of `StdMapClassValueChild` given +int(3) diff --git a/tests/aot/std-ordered-map/004.phpt b/tests/aot/std-ordered-map/004.phpt index 86f03042..8e03970a 100644 --- a/tests/aot/std-ordered-map/004.phpt +++ b/tests/aot/std-ordered-map/004.phpt @@ -1,5 +1,5 @@ --TEST-- -std containers: exact class value type +std containers: class value type accepts subclasses --FILE-- getValue()); } catch (Throwable $e) { echo $e->getMessage(), "\n"; } @@ -63,5 +64,5 @@ int(1) int(2) int(3) int(4) -The parameter `object` must be instance of class `StdContainerClassValue`, object of `StdContainerClassValueChild` given +int(5) The parameter `object` must be instance of class `StdContainerClassValue`, object of `StdContainerClassValueOther` given diff --git a/tests/aot/std-vector/003.phpt b/tests/aot/std-vector/003.phpt index a398533d..eb288a62 100644 --- a/tests/aot/std-vector/003.phpt +++ b/tests/aot/std-vector/003.phpt @@ -1,5 +1,5 @@ --TEST-- -std vector: exact class value type +std vector: class value type accepts subclasses --FILE-- getValue()); } catch (Throwable $e) { echo $e->getMessage(), "\n"; } @@ -41,4 +42,4 @@ function main() { --EXPECT-- int(1) int(2) -The parameter `object` must be instance of class `StdVectorClassValue`, object of `StdVectorClassValueChild` given +int(3) diff --git a/tests/aot/std-vector/004.phpt b/tests/aot/std-vector/004.phpt index 929c344b..63a8ae37 100644 --- a/tests/aot/std-vector/004.phpt +++ b/tests/aot/std-vector/004.phpt @@ -1,5 +1,5 @@ --TEST-- -std vector: exact class value type checks typed parameter at runtime +std vector: class value type accepts typed parameter subclass at runtime --FILE-- value); } catch (Throwable $e) { echo $e->getMessage(), "\n"; } @@ -29,4 +30,4 @@ function main() { } ?> --EXPECT-- -The parameter `object` must be instance of class `StdVectorRuntimeClassValue`, object of `StdVectorRuntimeClassValueChild` given +int(1) diff --git a/tests/aot/std-vector/005.phpt b/tests/aot/std-vector/005.phpt index e84d69c3..43a67a3b 100644 --- a/tests/aot/std-vector/005.phpt +++ b/tests/aot/std-vector/005.phpt @@ -1,5 +1,5 @@ --TEST-- -std vector: exact class value type rejects non-object value +std vector: class value type rejects non-object value --FILE-- value; + } +} + +class StdContainerInterfaceOther +{ +} + +function std_container_interface_mixed(mixed $value): mixed +{ + return $value; +} + +function main() { + $vector = std::vector(StdContainerInterfaceValue::class); + $vector[] = new StdContainerInterfaceImpl(1); + $vector[] = std_container_interface_mixed(new StdContainerInterfaceImpl(2)); + var_dump($vector[0]->getValue()); + var_dump($vector[1]->getValue()); + + $array = std::array(StdContainerInterfaceValue::class, 1); + $array[0] = std_container_interface_mixed(new StdContainerInterfaceImpl(3)); + var_dump($array[0]->getValue()); + + $map = std::map(complex_types::type_str, StdContainerInterfaceValue::class); + $map["item"] = std_container_interface_mixed(new StdContainerInterfaceImpl(4)); + var_dump($map["item"]->getValue()); + + $ordered = std::ordered_map(complex_types::type_str, StdContainerInterfaceValue::class); + $ordered["item"] = std_container_interface_mixed(new StdContainerInterfaceImpl(5)); + var_dump($ordered["item"]->getValue()); + + try { + $vector[] = std_container_interface_mixed(new StdContainerInterfaceOther()); + } catch (Throwable $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--EXPECT-- +int(1) +int(2) +int(3) +int(4) +int(5) +The parameter `object` must be instance of class `StdContainerInterfaceValue`, object of `StdContainerInterfaceOther` given diff --git a/tests/aot/std-vector/015.phpt b/tests/aot/std-vector/015.phpt new file mode 100644 index 00000000..3670cb86 --- /dev/null +++ b/tests/aot/std-vector/015.phpt @@ -0,0 +1,44 @@ +--TEST-- +std containers: abstract class value type +--FILE-- +value; + } +} + +class StdContainerAbstractOther +{ +} + +function std_container_abstract_mixed(mixed $value): mixed +{ + return $value; +} + +function main() { + $vector = std::vector(StdContainerAbstractValue::class); + $vector[] = std_container_abstract_mixed(new StdContainerAbstractChild(1)); + var_dump($vector[0]->getValue()); + + try { + $vector[] = std_container_abstract_mixed(new StdContainerAbstractOther()); + } catch (Throwable $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--EXPECT-- +int(1) +The parameter `object` must be instance of class `StdContainerAbstractValue`, object of `StdContainerAbstractOther` given diff --git a/tests/aot/stdlib/abs_edge.phpt b/tests/aot/stdlib/abs_edge.phpt index 1d856287..326ae42a 100644 --- a/tests/aot/stdlib/abs_edge.phpt +++ b/tests/aot/stdlib/abs_edge.phpt @@ -2,7 +2,8 @@ abs edge cases: PHP_INT_MIN and -0.0 --FILE--