feat(aot): 支持类继承、接口及抽象类的类型兼容与运行时检查

允许子类对象赋值给父类/接口/抽象类声明的类型变量和容器,引入 declaredClass 区分声明类型与实际类型,对无法静态证明的继承关系添加运行时类型
pull/15/head
韩天峰 2 months ago
parent f0e025dad4
commit c3f0151e87
  1. 4
      docs/INCOMPATIBLE_PHP_FEATURES.md
  2. 26
      examples/type-elimination.php
  3. 12
      phpunit/code/external-library-subclass-param.php
  4. 19
      phpunit/code/interface-declared-object-mismatch.php
  5. 18
      phpunit/code/interface-param-mismatch.php
  6. 14
      phpunit/code/interface-return-mismatch.php
  7. 10
      phpunit/code/loop/internal-constant-for-bound.php
  8. 15
      phpunit/code/re-assign-parent-to-child-object.php
  9. 47
      phpunit/src/AssignTest.php
  10. 209
      phpunit/src/CompilerBaseApiTest.php
  11. 4
      phpunit/src/Context/FunctionContextTest.php
  12. 36
      phpunit/src/LoopOptimizerTest.php
  13. 6
      phpunit/src/NativePropertyTest.php
  14. 17
      phpunit/src/PreprocessorTest.php
  15. 4
      phpunit/src/UndefineTest.php
  16. 2
      phpunit/src/UniversalMethodCallTest.php
  17. 7
      src/Php/ArgInfo.php
  18. 334
      src/Php/CompilerBase.php
  19. 11
      src/Php/Context/FunctionContext.php
  20. 43
      src/Php/Parser/AssignOpTrait.php
  21. 10
      src/Php/Parser/StdContainerTrait.php
  22. 5
      src/Php/Preprocessor.php
  23. 210
      src/Php/Translator.php
  24. 1
      src/Php/UniversalMethodCall.php
  25. 38
      tests/aot/class/abstract-return-typed-object.phpt
  26. 60
      tests/aot/class/interface-declared-object-assign.phpt
  27. 48
      tests/aot/class/interface-dynamic-call-zend-check.phpt
  28. 43
      tests/aot/class/interface-native-call-toobject-opt.phpt
  29. 61
      tests/aot/class/interface-param-return-check.phpt
  30. 12
      tests/aot/class/objval-parent.phpt
  31. 129
      tests/aot/class/typed-object-assign-any.phpt
  32. 57
      tests/aot/keyword_method/to-any-to-ref.phpt
  33. 12
      tests/aot/loop/for-internal-constant-bound.phpt
  34. 31
      tests/aot/ref/reuse.phpt
  35. 5
      tests/aot/std-array/006.phpt
  36. 5
      tests/aot/std-map/004.phpt
  37. 5
      tests/aot/std-ordered-map/004.phpt
  38. 5
      tests/aot/std-vector/003.phpt
  39. 5
      tests/aot/std-vector/004.phpt
  40. 2
      tests/aot/std-vector/005.phpt
  41. 63
      tests/aot/std-vector/014.phpt
  42. 44
      tests/aot/std-vector/015.phpt
  43. 3
      tests/aot/stdlib/abs_edge.phpt

@ -35,8 +35,8 @@
- 闭包和箭头函数不支持引用参数。 - 闭包和箭头函数不支持引用参数。
- 引用赋值的右侧必须是编译器可直接定位的变量、数组元素或对象属性;不支持从调用结果或复杂静态属性表达式建立引用。 - 引用赋值的右侧必须是编译器可直接定位的变量、数组元素或对象属性;不支持从调用结果或复杂静态属性表达式建立引用。
- 动态调用、闭包调用等编译期无法确定参数签名的调用,不能自动转换引用参数;需要显式使用 `refval()` - 动态调用、闭包调用等编译期无法确定参数签名的调用,不能自动转换引用参数;需要显式使用 `refval()` 或等价关键词方法 `toRef()`
- `refval()` 只接受变量、数组元素或对象属性。 - `refval()` / `toRef()` 只接受变量、数组元素或对象属性。
- 带 unpack 且尾部追加 named arguments 的调用会退化为动态调用,不能使用 native call。 - 带 unpack 且尾部追加 named arguments 的调用会退化为动态调用,不能使用 native call。
## 对象模型 ## 对象模型

@ -0,0 +1,26 @@
<?php
interface TestInterface
{
public function test(): TestInterface;
}
class TestClass implements TestInterface
{
public function test(): TestInterface
{
return $this;
}
public function foo()
{
var_dump(__METHOD__);
return $this;
}
}
function main()
{
$test = new TestClass();
$test = $test->test();
$test->foo();
}

@ -0,0 +1,12 @@
<?php
use PhpParser\Node\Expr;
function accepts_php_parser_expr(Expr $expr): void
{
}
function main(): void
{
accepts_php_parser_expr(new Expr\Variable('value'));
}

@ -0,0 +1,19 @@
<?php
interface InterfaceDeclaredAssignContract
{
}
class InterfaceDeclaredAssignImpl implements InterfaceDeclaredAssignContract
{
}
class InterfaceDeclaredAssignOther
{
}
function test_interface_declared_assign(InterfaceDeclaredAssignContract $object): void
{
$object = new InterfaceDeclaredAssignImpl();
$object = new InterfaceDeclaredAssignOther();
}

@ -0,0 +1,18 @@
<?php
interface InterfaceParamMismatchContract
{
}
class InterfaceParamMismatchOther
{
}
function interface_param_mismatch(InterfaceParamMismatchContract $object): void
{
}
function main(): void
{
interface_param_mismatch(new InterfaceParamMismatchOther());
}

@ -0,0 +1,14 @@
<?php
interface InterfaceReturnMismatchContract
{
}
class InterfaceReturnMismatchOther
{
}
function interface_return_mismatch(): InterfaceReturnMismatchContract
{
return new InterfaceReturnMismatchOther();
}

@ -0,0 +1,10 @@
<?php
function loopInternalConstantForBound(): bool
{
$sum = 0;
for ($i = 0; $i < PHP_FD_SETSIZE; $i++) {
$sum += $i & 1;
}
return $sum > 0;
}

@ -0,0 +1,15 @@
<?php
class TypedObjectAssignBase
{
}
class TypedObjectAssignChild extends TypedObjectAssignBase
{
}
function main(): void
{
$child = new TypedObjectAssignChild();
$child = new TypedObjectAssignBase();
}

@ -14,14 +14,43 @@ class AssignTest extends \BaseTest
$this->exec('Cannot re-assign typed object `$obj1` from `stdClass` to `ArrayObject`', 're-assign-2.php'); $this->exec('Cannot re-assign typed object `$obj1` from `stdClass` to `ArrayObject`', 're-assign-2.php');
} }
public function testStdContainerStaticClassMismatch() public function testCannotAssignParentObjectToChildTypedObject()
{ {
$this->exec( $this->exec(
'Cannot assign object of class `StdContainerStaticChild` to std container value of class `StdContainerStaticBase`', 'Cannot re-assign typed object `$child` from `TypedObjectAssignChild` to `TypedObjectAssignBase`',
'std-container-static-class-mismatch.php' '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) === // === Object value assigned to non-object variable (right side is New_ expr) ===
public function testObjectToInt() 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'); $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( $this->compile('object-prop-subclass-mismatch.php');
'Cannot assign object of class `TypedObjectPropChild` to object property `prop` of class `TypedObjectPropBase`', }
'object-prop-subclass-mismatch.php'
); public function testCanPassExternalLibrarySubclassToParentParameter()
{
$this->compile('external-library-subclass-param.php');
} }
// === Str / Array value assigned to non-object scalar variable === // === Str / Array value assigned to non-object scalar variable ===

@ -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 public function testParseProjectYamlSupportsCliStyleModeAndOutputAliases(): void
{ {
$projectFile = $this->createProjectFile(<<<'YAML' $projectFile = $this->createProjectFile(<<<'YAML'
@ -380,6 +392,199 @@ YAML);
$this->assertNotContains(realpath($projectDir . '/skipped/nested.php'), $files); $this->assertNotContains(realpath($projectDir . '/skipped/nested.php'), $files);
} }
public function testParseProjectYamlSupportsConditionalSourcesByPhpVersion(): void
{
$futureVersion = PHP_VERSION_ID + 10000;
$projectFile = $this->createProjectFile(<<<YAML
sources:
- main.php
- path: php-current.php
if: PHP_VERSION_ID >= 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', "<?php\nfunction php_current_source(): void {}\n");
file_put_contents($projectDir . '/php-id-reversed.php', "<?php\nfunction php_id_reversed_source(): void {}\n");
$files = $this->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', "<?php\nfunction if_before_path_source(): void {}\n");
$files = $this->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', "<?php\nfunction composite_source(): void {}\n");
$files = $this->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(<<<YAML
sources:
- path: php-version-current.php
if: PHP_VERSION >= "{$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', "<?php\nfunction php_version_current_source(): void {}\n");
file_put_contents($projectDir . '/php-version-reversed.php', "<?php\nfunction php_version_reversed_source(): void {}\n");
$files = $this->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(<<<YAML
sources:
- path: op-lt.php
if: PHP_VERSION lt "{$current}.1"
- path: op-le.php
if: PHP_VERSION le "{$current}"
- path: op-gt.php
if: PHP_VERSION gt "0.0.0"
- path: op-ge.php
if: PHP_VERSION ge "{$current}"
- path: op-eq.php
if: PHP_VERSION eq "{$current}"
- path: op-ne.php
if: PHP_VERSION ne "0.0.0"
- path: op-symbol-eq.php
if: PHP_VERSION = "{$current}"
- path: op-symbol-ne.php
if: PHP_VERSION <> "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', "<?php\nfunction op_" . str_replace('-', '_', $name) . "(): void {}\n");
}
$files = $this->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(<<<YAML
sources:
- path: os-current.php
if: PHP_OS_FAMILY == "{$osFamily}"
- path: os-not-windows.php
if: PHP_OS_FAMILY != "{$otherFamily}"
- path: os-reversed.php
if: '"{$osFamily}" == PHP_OS_FAMILY'
- path: os-composite.php
if: PHP_OS_FAMILY == "{$osFamily}" && PHP_VERSION_ID >= 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', "<?php\nfunction os_" . str_replace('-', '_', $name) . "(): void {}\n");
}
$files = $this->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 public function testCCompileCommandOptionsKeepCommonUserConfiguration(): void
{ {
$this->setPropertyValue('userIncludePaths', ['/user/include']); $this->setPropertyValue('userIncludePaths', ['/user/include']);
@ -655,7 +860,7 @@ YAML);
public function testGetNamespacedFuncNameWithUseFunction(): void public function testGetNamespacedFuncNameWithUseFunction(): void
{ {
$this->setPropertyValue('useFunctions', [ $this->setPropertyValue('useFunctions', [
'helper_func' => 'App\\Lib', 'helper_func' => 'App\\Lib\\helper_func',
]); ]);
$this->assertEquals( $this->assertEquals(
'App\\Lib\\helper_func', 'App\\Lib\\helper_func',
@ -678,7 +883,7 @@ YAML);
public function testGetNamespacedFuncNameNotInUseFunctions(): void public function testGetNamespacedFuncNameNotInUseFunctions(): void
{ {
$this->setPropertyValue('useFunctions', ['other' => 'Some\\Ns']); $this->setPropertyValue('useFunctions', ['other' => 'Some\\Ns\\other']);
$this->assertEquals( $this->assertEquals(
'my_func', 'my_func',
$this->compiler->getNamespacedFuncName('my_func') $this->compiler->getNamespacedFuncName('my_func')

@ -29,7 +29,6 @@ class FunctionContextTest extends TestCase
$this->assertSame(0, $ctx->scopeLevel); $this->assertSame(0, $ctx->scopeLevel);
$this->assertFalse($ctx->inLoop); $this->assertFalse($ctx->inLoop);
$this->assertFalse($ctx->inClosure); $this->assertFalse($ctx->inClosure);
$this->assertFalse($ctx->inAssignExpr);
} }
public function testEnterScopeIncrementsLevel(): void public function testEnterScopeIncrementsLevel(): void
@ -72,9 +71,6 @@ class FunctionContextTest extends TestCase
$ctx->inClosure = true; $ctx->inClosure = true;
$this->assertTrue($ctx->inClosure); $this->assertTrue($ctx->inClosure);
$ctx->inAssignExpr = true;
$this->assertTrue($ctx->inAssignExpr);
$ctx->tmpVarIndex = 5; $ctx->tmpVarIndex = 5;
$this->assertSame(5, $ctx->tmpVarIndex); $this->assertSame(5, $ctx->tmpVarIndex);
} }

@ -0,0 +1,36 @@
<?php
use PhpAot\Php\CompilerTest;
use PhpAot\Php\Exception\TestError;
class LoopOptimizerTest extends \BaseTest
{
private function compileToCpp(string $file): string
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$testFile = __DIR__ . '/../code/' . $file;
$compiler->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);
}
}

@ -5,7 +5,7 @@ use PhpAot\Php\Exception\TestError;
class NativePropertyTest extends \BaseTest class NativePropertyTest extends \BaseTest
{ {
private function compile(string $file): string private function compileNativeProperty(string $file): string
{ {
global $translator; global $translator;
@ -23,7 +23,7 @@ class NativePropertyTest extends \BaseTest
public function testFindNativePropertyUsesFullClassNameAcrossBranches(): void public function testFindNativePropertyUsesFullClassNameAcrossBranches(): void
{ {
try { try {
$this->compile('native-property-full-name.php'); $this->compileNativeProperty('native-property-full-name.php');
} catch (TestError $e) { } catch (TestError $e) {
$this->fail($e->getMessage()); $this->fail($e->getMessage());
} }
@ -32,7 +32,7 @@ class NativePropertyTest extends \BaseTest
public function testStaticStaticPropertyUsesDynamicCalledClassPath(): void public function testStaticStaticPropertyUsesDynamicCalledClassPath(): void
{ {
try { try {
$outputFile = $this->compile('native-property-full-name.php'); $outputFile = $this->compileNativeProperty('native-property-full-name.php');
} catch (TestError $e) { } catch (TestError $e) {
$this->fail($e->getMessage()); $this->fail($e->getMessage());
} }

@ -207,17 +207,20 @@ class PreprocessorTest extends TestCase
public function testGetParentClassWithNamespace(): void public function testGetParentClassWithNamespace(): void
{ {
$extends = new Node\Name('BaseController'); $this->setProperty('classExtends', [
$this->setProperty('namespace', 'App\\Controllers'); 'app\\controllers\\homecontroller' => 'app\\controllers\\basecontroller',
$result = $this->invokeMethod('getParentClass', $extends); ]);
$this->assertEquals('App\\Controllers\\BaseController', $result); $result = $this->compiler->getParentClass('App\\Controllers\\HomeController');
$this->assertEquals('app\\controllers\\basecontroller', $result);
} }
public function testGetParentClassFullyQualified(): void public function testGetParentClassFullyQualified(): void
{ {
$extends = new Node\Name\FullyQualified('App\\Entity\\Base'); $this->setProperty('classExtends', [
$result = $this->invokeMethod('getParentClass', $extends); 'app\\entity\\user' => 'app\\entity\\base',
$this->assertEquals('App\\Entity\\Base', $result); ]);
$result = $this->compiler->getParentClass('\\App\\Entity\\User');
$this->assertEquals('app\\entity\\base', $result);
} }
// ======================================================================== // ========================================================================

@ -20,11 +20,11 @@ class UndefineTest extends \BaseTest
public function testPropertyAccessOnUndefinedVar(): void 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 public function testMethodCallOnUndefinedVar(): void
{ {
$this->exec('The variable `$obj` is undefined', 'undefined-method-call.php'); $this->exec('The variable `$obj` is undefined', 'undefined-method-call.php');
} }
} }

@ -19,7 +19,7 @@ class UniversalMethodCallTest extends \BaseTest
public function testVoidMethodCall() public function testVoidMethodCall()
{ {
$this->exec('Cannot call method on void', 'void-method-call.php'); $this->compile('void-method-call.php');
} }
} }

@ -21,6 +21,13 @@ class ArgInfo
public ?ArrayInitPlan $arrayInitPlan = null; public ?ArrayInitPlan $arrayInitPlan = null;
public ?Expr $defaultValue = null; public ?Expr $defaultValue = null;
public string $class = ''; 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 $byRef = false;
public bool $variadic = false; public bool $variadic = false;
public bool $nullable = false; public bool $nullable = false;

@ -125,6 +125,8 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
'toBigFloat' => self::TYPE_BIGFLOAT, 'toBigFloat' => self::TYPE_BIGFLOAT,
'toDecimal' => self::TYPE_DECIMAL, 'toDecimal' => self::TYPE_DECIMAL,
'toObject' => self::TYPE_OBJECT, 'toObject' => self::TYPE_OBJECT,
'toAny' => self::TYPE_VAR,
'toRef' => self::TYPE_REF,
]; ];
private const array STREAM_FUNCTIONS = [ private const array STREAM_FUNCTIONS = [
@ -529,6 +531,17 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
return $this->context->objects[$object] ?? 'stdClass'; 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 public function parseExpr(NodeAbstract $expr): string
{ {
if ($expr->hasAttribute('replace')) { if ($expr->hasAttribute('replace')) {
@ -1775,6 +1788,66 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
return ''; 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 protected function resolveClassNameArg(NodeAbstract $arg): string
{ {
if ($this->isScalarString($arg)) { if ($this->isScalarString($arg)) {
@ -1829,27 +1902,25 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$returnType = self::TYPE_VAR; $returnType = self::TYPE_VAR;
} }
$returnObjectCheckClass = '';
// 返回值的表达式是一个类的对象 // 返回值的表达式是一个类的对象
$objectClass = $this->detectClassOfExpr($v->expr); $objectClass = $this->detectDeclaredClassOfExpr($v->expr);
$returnClass = $this->getReturnClass(); $returnClass = $this->context->inClosure ? '' : $this->getReturnClass();
if ($returnClass) { if ($returnClass) {
if (!$objectClass or $this->hasInterface($objectClass)) { if ($objectClass === '') {
// TODO 返回值的类型无法确定,或者是一个接口,无法继承关系,需要插入动态类型检测代码 $returnObjectCheckClass = $returnClass;
} elseif (!$this->isInheritedFrom($objectClass, $returnClass)) { } elseif (!$this->isObjectClassStaticallyAssignableTo($objectClass, $returnClass)) {
$this->fatalError($v, 'The return type is `' . $returnClass . '`, cannot return an instance of `' . $objectClass . '`'); if ($this->isKnownConcreteObjectExpr($v->expr, $objectClass)) {
} $this->fatalError($v, 'The return type is `' . $returnClass . '`, cannot return an instance of `' . $objectClass . '`');
// 把子类当做父类返回时,父类必须是抽象类或者接口 }
// 仅原生类进行静态检查,若类不存在,说明该类是动态类,无法进行编译期验证 $returnObjectCheckClass = $returnClass;
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");
} }
} }
$exprCode = $this->convertExprType($expr, $returnType, $type); $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 // Union/nullable return type: always use tmpVar for runtime check
if ($this->shouldCheckClosureReturnType()) { if ($this->shouldCheckClosureReturnType()) {
[$code, $tmpVar] = $this->genClosureCheckedReturnAssignment($exprCode); [$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 protected function addObject(string $name, string $class): void
{ {
// 接口、抽象类、非原生类,无法作为 TypedObject 使用 // Interfaces have no concrete method body for native calls. Abstract classes may have concrete methods.
if (!$this->isInterface($class) and !$this->isAbstractClass($class) and if ($this->isInterface($class)) {
($this->isNativeClass($class) or $this->isInternalClass($class))) { $this->context->declaredObjects[$name] = $class;
} elseif ($this->isNativeClass($class) or $this->isInternalClass($class)) {
$this->context->objects[$name] = $class; $this->context->objects[$name] = $class;
} }
} }
@ -3716,7 +3788,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
} }
$namedArgs[$arg->name->name] = true; $namedArgs[$arg->name->name] = true;
$byRef = $funcName && $this->isReferenceNamedArgument($funcName, $className, $arg->name->name); $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->parseReferenceCallArgValue($arg)
: $this->parseCallArgValue($arg); : $this->parseCallArgValue($arg);
if ($separateNamedArgs) { 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); $this->addPositionalCallArg($array . '.itemRef(' . $this->identifierToStr($arg->value->dim) . ')', $arrayArgsVar, $list_args);
continue; continue;
} }
} elseif ($this->isFuncCallExpr($arg->value)) { } elseif ($this->isReferenceWrapperCall($arg->value)) {
if ($this->isNameExpr($arg->value->name) and $arg->value->name->toString() === 'refval') { $inner = $this->unwrapReferenceWrapperCall($arg->value, $arg);
if (count($arg->value->args) !== 1) { if ($this->isVarExpr($inner)) {
$this->fatalError($arg, 'The refval function only accepts one parameter'); $name = $this->parseVariable($inner);
} $arg->value = $inner;
$inner = $arg->value->args[0]->value; $this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args);
if ($this->isVarExpr($inner)) { continue;
$name = $this->parseVariable($inner); }
// 消除 refval() 函数调用,直接使用变量 $expr = $this->expandRefvalExpr($inner, $arg);
$arg->value = $inner; if ($expr !== null) {
$this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args); $this->addPositionalCallArg($expr, $arrayArgsVar, $list_args);
continue; 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');
} }
$this->fatalError($arg, 'The refval function only accepts a variable, array element, or object property');
} else { } else {
if ($byRef) { if ($byRef) {
if ($this->isScalar($arg->value)) { if ($this->isScalar($arg->value)) {
@ -3934,11 +4000,8 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
protected function parseReferenceCallArgValue(Node\Arg $arg): string protected function parseReferenceCallArgValue(Node\Arg $arg): string
{ {
if ($this->isRefvalCall($arg->value)) { if ($this->isReferenceWrapperCall($arg->value)) {
if (count($arg->value->args) !== 1) { $arg->value = $this->unwrapReferenceWrapperCall($arg->value, $arg);
$this->fatalError($arg, 'The refval function only accepts one parameter');
}
$arg->value = $arg->value->args[0]->value;
} }
if ($this->isVarExpr($arg->value)) { if ($this->isVarExpr($arg->value)) {
@ -3976,6 +4039,37 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
return '&' . $tmpRef; 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++ 引用表达式。 * 展开 refval() 调用中的数组元素或对象属性,返回对应的 C++ 引用表达式。
* 若为普通变量则返回 null,由调用方自行处理。 * 若为普通变量则返回 null,由调用方自行处理。
@ -4654,6 +4748,9 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
if ($name === 'PHP_EOL') { if ($name === 'PHP_EOL') {
return '"' . $this->escapeString(PHP_EOL) . '"'; return '"' . $this->escapeString(PHP_EOL) . '"';
} }
if ($this->isInternalScalarConstant($name)) {
return $this->getInternalScalarConstantValue($name);
}
if ($scalar) { if ($scalar) {
return constant($expr->name); return constant($expr->name);
} }
@ -4768,8 +4865,35 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
protected function isInheritedFrom(string $class, string $expected): bool 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)); $internal = ($this->isInternalClass($expected) or $this->isInternalInterface($expected));
$isInterface = ($this->hasInterface($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)) { if (!$this->hasClass($class)) {
return true; return true;
@ -4789,6 +4913,9 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
return true; return true;
} }
if (!$this->hasInterface($check)) { if (!$this->hasInterface($check)) {
if ($internal && $this->isInternalInterface($check) && is_subclass_of($check, $expected)) {
return true;
}
continue; continue;
} }
$interfaceDef = $this->getInterface($check); $interfaceDef = $this->getInterface($check);
@ -4796,9 +4923,6 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$stack[] = $parentIface; $stack[] = $parentIface;
} }
} }
if (is_subclass_of($iface, $expected)) {
return true;
}
} }
} else { } else {
if (strcasecmp($class, $expected) === 0) { if (strcasecmp($class, $expected) === 0) {
@ -4817,23 +4941,48 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
return false; return false;
} }
$class = $classDef->extends; $class = $classDef->extends;
if ($this->isInternalClass($class)) {
// 项目类可以继承内置类。进入内置父类链后,后续关系交给 Zend 判断;
// 但 expected 也必须是内置类/接口,否则不能跨到外部用户类命名空间做运行时反射。
return $internal && is_subclass_of($class, $expected);
}
$classDef = $this->getClass($class); $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 protected function getTypeConvertedArg(Node\Arg $arg, ArgInfo $argInfo): string
{ {
$type = $this->detectTypeOfExpr($arg->value); $type = $this->detectTypeOfExpr($arg->value);
$this->assertExprCanBeUsedAsValue($arg->value, 'function argument'); $this->assertExprCanBeUsedAsValue($arg->value, 'function argument');
if ($argInfo->byRef) { if ($argInfo->byRef) {
if ($this->isRefvalCall($arg->value)) { if ($this->isReferenceWrapperCall($arg->value)) {
if (count($arg->value->args) !== 1) { $inner = $this->unwrapReferenceWrapperCall($arg->value, $arg);
$this->fatalError($arg, 'The refval function only accepts one parameter');
}
$inner = $arg->value->args[0]->value;
if ($this->isVarExpr($inner)) { if ($this->isVarExpr($inner)) {
// 消除 refval() 函数调用,直接使用变量
$arg->value = $inner; $arg->value = $inner;
} else { } else {
$expr = $this->expandRefvalExpr($inner, $arg); $expr = $this->expandRefvalExpr($inner, $arg);
@ -4866,17 +5015,25 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
} }
if ($argInfo->type === self::TYPE_OBJECT) { if ($argInfo->type === self::TYPE_OBJECT) {
if ($this->isVarExpr($arg->value)) { $declaredClass = $argInfo->declaredClass ?: $argInfo->class;
$object = $this->parseVariable($arg->value); if ($declaredClass !== '') {
if ($this->isTypedObject($object)) { $class = $this->detectDeclaredClassOfExpr($arg->value);
$class = $this->getObjectType($object); if ($class !== '') {
if ($class and $argInfo->class and !$this->isInheritedFrom($class, $argInfo->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); $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); 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)) { if ($def->class === '' or $this->isAbstractClass($def->class) or $this->isInterface($def->class) or !$this->hasClass($def->class)) {
// 属性 declared class 若是接口、抽象类或动态类,当前属性布局优化无法静态确认最终对象类型。
// 不在这里 fatal;后续 wrapObjectPropertyAssignTypeCheck() 会在需要时插入运行时检查。
return; return;
} }
@ -4986,7 +5145,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
if ($rightClass === '') { if ($rightClass === '') {
return; return;
} }
if ($rightClass !== $def->class) { if (!$this->isObjectClassStaticallyAssignableTo($rightClass, $def->class)) {
$this->fatalError( $this->fatalError(
$left, $left,
"Cannot assign object of class `{$rightClass}` to {$label} `{$propName}` of class `{$def->class}`" "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; 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 protected function parseSwitch(Node\Stmt\Switch_ $v): string
{ {
$cond = $v->cond; $cond = $v->cond;
@ -6129,7 +6323,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
} }
$receiver = $this->parseExpr($expr->args[0]->value); $receiver = $this->parseExpr($expr->args[0]->value);
$className = $this->resolveClassNameArg($expr->args[1]->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 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 . ')'; return 'php::toObject(' . $receiver . ')';
} }
$className = $this->resolveClassNameArg($expr->args[0]->value); $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 protected function parseMethodCall(Expr\MethodCall $expr): string
@ -6173,6 +6375,12 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
if ($methodName === 'toObject') { if ($methodName === 'toObject') {
return $this->genToObjectCall($expr, $object); 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); return $this->genToConvertCall($object, $methodName, $receiverType);
} }
// __ keyword extensions // __ keyword extensions
@ -6516,7 +6724,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
return $this->getNativePropertyAccess($expr)?->getClassDef(); return $this->getNativePropertyAccess($expr)?->getClassDef();
} }
private function getNativePropertyAccess(NodeAbstract $expr): ?NativePropertyAccess public function getNativePropertyAccess(NodeAbstract $expr): ?NativePropertyAccess
{ {
$access = $expr->getAttribute('nativePropertyAccess'); $access = $expr->getAttribute('nativePropertyAccess');
return $access instanceof NativePropertyAccess ? $access : null; return $access instanceof NativePropertyAccess ? $access : null;

@ -29,6 +29,13 @@ class FunctionContext
*/ */
public array $objects = []; public array $objects = [];
/**
* Declared object constraints that are not used for native-call dispatch.
*
* @var array<string, string>
*/
public array $declaredObjects = [];
/** /**
* @var array<string, array> * @var array<string, array>
*/ */
@ -76,6 +83,7 @@ class FunctionContext
$this->staticVars = []; $this->staticVars = [];
$this->arguments = []; $this->arguments = [];
$this->objects = []; $this->objects = [];
$this->declaredObjects = [];
$this->stdArrays = []; $this->stdArrays = [];
$this->stdContainers = []; $this->stdContainers = [];
$this->objectProps = []; $this->objectProps = [];
@ -106,10 +114,11 @@ class FunctionContext
unset($this->scopeLayouts[$this->scopeLevel]); 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->localVars = $localVars;
$this->tmpVarIndex = $tmpVarIndex; $this->tmpVarIndex = $tmpVarIndex;
$this->declaredObjects = $declaredObjects;
$this->beforeStmtLines = []; $this->beforeStmtLines = [];
$this->afterStmtLines = []; $this->afterStmtLines = [];
$this->objectProps = []; $this->objectProps = [];

@ -147,6 +147,8 @@ trait AssignOpTrait
$propertyWriteTarget = $this->preparePropertyWriteTarget($left); $propertyWriteTarget = $this->preparePropertyWriteTarget($left);
$type = $this->detectTypeOfExpr($right); $type = $this->detectTypeOfExpr($right);
$finalVarType = $this->getNormalAssignType($type); $finalVarType = $this->getNormalAssignType($type);
$runtimeObjectAssignClass = '';
$rightExprOverride = null;
if ($type === self::TYPE_VOID) { if ($type === self::TYPE_VOID) {
$type = self::TYPE_VAR; $type = self::TYPE_VAR;
} }
@ -169,11 +171,16 @@ trait AssignOpTrait
if (!$this->hasVar($var)) { if (!$this->hasVar($var)) {
$this->addLocalVar($var, self::TYPE_OBJECT); $this->addLocalVar($var, self::TYPE_OBJECT);
$this->addObject($var, $rightClass); $this->addObject($var, $rightClass);
} elseif ($this->isTypedObject($var)) { } elseif (($leftClass = $this->getDeclaredObjectType($var)) !== '') {
$leftClass = $this->getObjectType($var); if ($this->isObjectClassStaticallyAssignableTo($rightClass, $leftClass)) {
// 对象的类不一致,不能互相赋值,必须使用 toObject() 对齐类型 // A child object can be assigned to a parent typed object.
// 注意这里必须使用绝对相等比较,即使存在继承关系,类的方法也可能不一致 } elseif ($this->isInterface($rightClass) || $this->isAbstractClass($rightClass) || $this->isObjectClassStaticallyAssignableTo($leftClass, $rightClass)) {
if ($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}`"); $this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$rightClass}`");
} }
} else { } else {
@ -199,8 +206,9 @@ trait AssignOpTrait
if (!$this->hasVar($var)) { if (!$this->hasVar($var)) {
$this->addLocalVar($var, $type); $this->addLocalVar($var, $type);
$finalVarType = $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 { } else {
$type = $type === self::TYPE_VOID ? self::TYPE_VAR : $type; $type = $type === self::TYPE_VOID ? self::TYPE_VAR : $type;
} }
@ -241,10 +249,16 @@ trait AssignOpTrait
$rightVar = $this->parseIdentifier($right); $rightVar = $this->parseIdentifier($right);
$type = $this->isStdContainer($rightVar) ? self::TYPE_ARRAY : $this->getVarType($rightVar); $type = $this->isStdContainer($rightVar) ? self::TYPE_ARRAY : $this->getVarType($rightVar);
$finalVarType = $this->getNormalAssignType($type); $finalVarType = $this->getNormalAssignType($type);
if ($this->isTypedObject($rightVar) and $this->isTypedObject($var)) { $leftClass = $this->getDeclaredObjectType($var);
$leftClass = $this->getObjectType($var); $rightClass = $this->getDeclaredObjectType($rightVar);
$rightClass = $this->getObjectType($rightVar); if ($leftClass !== '' and $rightClass !== '') {
$this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$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 块中声明的变量,可以在块外使用 // 变量第一次被赋值,确定其类型,由于 PHP 的变量作用域是 function 级的,在 for/while 块中声明的变量,可以在块外使用
@ -255,6 +269,10 @@ trait AssignOpTrait
} else { } else {
$finalVarType = $this->getVarType($var); $finalVarType = $this->getVarType($var);
$this->checkVarAssignExpr($left, $finalVarType, $type); $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)) { } elseif ($this->isPropertyFetch($left) and !$this->isNativePropertyAccess($left)) {
@ -277,10 +295,13 @@ trait AssignOpTrait
} }
$var = $this->parseWritableIdentifier($left); $var = $this->parseWritableIdentifier($left);
$rightExpr = $this->parseAssignRightExpr($right); $rightExpr = $rightExprOverride ?? $this->parseAssignRightExpr($right);
if ($propertyWriteTarget !== null) { if ($propertyWriteTarget !== null) {
$rightExpr = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $right, $rightExpr); $rightExpr = $this->wrapPropertyWriteTypeCheck($propertyWriteTarget, $right, $rightExpr);
} }
if ($runtimeObjectAssignClass !== '') {
$rightExpr = 'php::toObject(' . $rightExpr . ', ' . $this->getClassEntryPtr($runtimeObjectAssignClass) . ')';
}
$leftExprType = $this->detectTypeOfExpr($left); $leftExprType = $this->detectTypeOfExpr($left);
$rightExprType = $this->detectTypeOfExpr($right); $rightExprType = $this->detectTypeOfExpr($right);
if ($finalVarType === self::TYPE_VAR) { if ($finalVarType === self::TYPE_VAR) {

@ -541,12 +541,6 @@ trait StdContainerTrait
} else { } else {
$class = $this->getNamespacedClassName($class); $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; return $class;
} }
@ -563,12 +557,12 @@ trait StdContainerTrait
} }
$rightClass = $this->detectClassOfExpr($expr); $rightClass = $this->detectClassOfExpr($expr);
if ($rightClass !== '') { 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}`"); $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 protected function convertStdVarBackedExpr(string $targetType, string $valueExpr, NodeAbstract $expr): string

@ -266,7 +266,10 @@ class Preprocessor extends CompilerBase
) { ) {
$argInfo->explicitMixed = in_array(strtolower($this->parseIdentifier($param->type)), ['mixed', 'any'], true); $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; $argInfo->class = $class;
} }
return $type; return $type;

@ -594,7 +594,7 @@ class Translator extends Preprocessor
$this->sourceDirs[] = $path; $this->sourceDirs[] = $path;
} else { } else {
$ext = pathinfo($path, PATHINFO_EXTENSION); $ext = pathinfo($path, PATHINFO_EXTENSION);
if ($ext === 'yml') { if ($ext === 'yml' || $ext === 'yaml') {
// YAML 配置模式:先解析 YAML // YAML 配置模式:先解析 YAML
$list = $this->parseProjectYaml($path); $list = $this->parseProjectYaml($path);
} elseif ($ext === 'php') { } elseif ($ext === 'php') {
@ -2031,7 +2031,11 @@ CODE;
$this->error('`sources` must be array'); $this->error('`sources` must be array');
} }
$list = []; $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); $realPath = $this->getAbsolutePath($src, $projectDir);
if (!$realPath) { if (!$realPath) {
$this->error('Source file not exists: `' . $src . '`'); $this->error('Source file not exists: `' . $src . '`');
@ -2264,6 +2268,194 @@ CODE;
return $this->filterIgnoredFiles($list); 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('/(?<![&])&(?!&)|(?<![|])\|(?!\|)/', $expr)) {
$this->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 protected function getInternalCeInfo(string $ce): array
{ {
return [ return [
@ -2920,7 +3112,12 @@ CODE;
} }
} }
$cppType = $this->getDefaultArgumentType($argInfo); $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; $cppCode .= $this->getIndent() . $cppType . ' ' . $var . ' = ' . $expr . ';' . PHP_EOL;
} }
$callParams .= 'arg_' . $argInfo->name . ','; $callParams .= 'arg_' . $argInfo->name . ',';
@ -3110,8 +3307,8 @@ CODE;
} }
foreach ($this->functionDef->argInfoList as $argInfo) { foreach ($this->functionDef->argInfoList as $argInfo) {
$this->addArgument($argInfo->name, $argInfo->variadic ? self::TYPE_ARRAY : $argInfo->type); $this->addArgument($argInfo->name, $argInfo->variadic ? self::TYPE_ARRAY : $argInfo->type);
if (!$argInfo->variadic and $argInfo->class) { if (!$argInfo->variadic and $argInfo->declaredClass) {
$this->addObject($argInfo->name, $argInfo->class); $this->addObject($argInfo->name, $argInfo->declaredClass);
} }
} }
@ -3119,6 +3316,7 @@ CODE;
if ($v->stmts) { if ($v->stmts) {
$oriLocalVars = $this->context->localVars; $oriLocalVars = $this->context->localVars;
$oriTmpVarIndex = $this->context->tmpVarIndex; $oriTmpVarIndex = $this->context->tmpVarIndex;
$oriDeclaredObjects = $this->context->declaredObjects;
/** SSA/e-SSA analysis for the current function. Built once per function, discarded with the context. */ /** 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 = new SsaBuilder($v->stmts, $this->functionDef->argInfoList);
$ssaBuilder->build(); $ssaBuilder->build();
@ -3131,7 +3329,7 @@ CODE;
$this->optimizeLoopVars($ssaBuilder); $this->optimizeLoopVars($ssaBuilder);
$this->optimizeObjectProps($ssaBuilder); $this->optimizeObjectProps($ssaBuilder);
} }
$this->context->resetAnalysisTemporaries($oriLocalVars, $oriTmpVarIndex); $this->context->resetAnalysisTemporaries($oriLocalVars, $oriTmpVarIndex, $oriDeclaredObjects);
} }
$stmts = ''; $stmts = '';

@ -374,6 +374,7 @@ trait UniversalMethodCall
'toBigInt' => 'php::BigInt::newInstance(' . $receiver . ')', 'toBigInt' => 'php::BigInt::newInstance(' . $receiver . ')',
'toBigFloat' => 'php::BigFloat::newInstance(' . $receiver . ')', 'toBigFloat' => 'php::BigFloat::newInstance(' . $receiver . ')',
'toDecimal' => 'php::Decimal::newInstance(' . $receiver . ')', 'toDecimal' => 'php::Decimal::newInstance(' . $receiver . ')',
'toAny' => $receiver,
default => $receiver, default => $receiver,
}; };
} }

@ -0,0 +1,38 @@
--TEST--
abstract class return type can be used as typed object
--FILE--
<?php
abstract class AbstractTypedObjectBase
{
public function concreteName(): string
{
return 'base:' . $this->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"

@ -0,0 +1,60 @@
--TEST--
interface declared object constrains assignment without native call typing
--FILE--
<?php
interface DeclaredObjectContract
{
public function name(): string;
}
class DeclaredObjectImpl implements DeclaredObjectContract
{
public function name(): string
{
return 'impl';
}
}
class DeclaredObjectOther
{
public function name(): string
{
return 'other';
}
}
function nextDeclaredObject(DeclaredObjectContract $object): DeclaredObjectContract
{
return $object;
}
function testDeclaredObject(DeclaredObjectContract $object): void
{
var_dump($object->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"

@ -0,0 +1,48 @@
--TEST--
dynamic call uses wrapper interface parameter type check
--ENV--
USE_ZEND_ALLOC=0
--FILE--
<?php
interface InterfaceDynamicCallCheckContract
{
public function name(): string;
}
class InterfaceDynamicCallCheckImpl implements InterfaceDynamicCallCheckContract
{
public function name(): string
{
return 'impl';
}
}
class InterfaceDynamicCallCheckOther
{
}
function acceptInterfaceDynamicCallCheck(InterfaceDynamicCallCheckContract $object): string
{
return $object->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

@ -0,0 +1,43 @@
--TEST--
interface parameter native call avoids redundant object check when statically safe
--FILE--
<?php
interface InterfaceNativeCallOptContract
{
public function name(): string;
}
class InterfaceNativeCallOptImpl implements InterfaceNativeCallOptContract
{
public function name(): string
{
return 'impl';
}
}
class InterfaceNativeCallOptOther
{
}
function acceptInterfaceNativeCallOpt(InterfaceNativeCallOptContract $object): string
{
return $object->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

@ -0,0 +1,61 @@
--TEST--
interface parameter and return type checks with dynamic object values
--FILE--
<?php
interface InterfaceCallCheckContract
{
public function name(): string;
}
class InterfaceCallCheckImpl implements InterfaceCallCheckContract
{
public function name(): string
{
return 'impl';
}
}
class InterfaceCallCheckOther
{
public function name(): string
{
return 'other';
}
}
function useInterfaceCallCheck(InterfaceCallCheckContract $object): string
{
return $object->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

@ -17,6 +17,10 @@ class Child extends Base {
public function castToParent($obj): Base { public function castToParent($obj): Base {
return objval($obj, parent::class); return objval($obj, parent::class);
} }
public function toParent($obj): Base {
return $obj->toObject(parent::class);
}
} }
function main() { function main() {
@ -25,8 +29,16 @@ function main() {
$result = $c->castToParent($b); $result = $c->castToParent($b);
var_dump($result->name()); var_dump($result->name());
$result = $c->castToParent($c);
var_dump($result->name());
$result = $c->toParent($c);
var_dump($result->name());
} }
?> ?>
--EXPECT-- --EXPECT--
string(4) "Base" string(4) "Base"
string(5) "Child"
string(5) "Child"

@ -0,0 +1,129 @@
--TEST--
typed object assignment from any uses runtime class check
--FILE--
<?php
class AssignAnyBase
{
public function name(): string
{
return 'base';
}
}
class AssignAnyChild extends AssignAnyBase
{
}
class AssignAnyBaseFactory
{
public function child(): AssignAnyBase
{
return new AssignAnyChild();
}
public function base(): AssignAnyBase
{
return new AssignAnyBase();
}
}
interface AssignAnyInterface
{
public function next(): AssignAnyInterface;
}
class AssignAnyImpl implements AssignAnyInterface
{
public function next(): AssignAnyInterface
{
return $this;
}
public function ok(): string
{
return 'impl';
}
}
class AssignAnyOther implements AssignAnyInterface
{
public function next(): AssignAnyInterface
{
return $this;
}
}
function main(): void
{
$base = new AssignAnyBase();
$base = any(new AssignAnyChild());
var_dump($base->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"

@ -0,0 +1,57 @@
--TEST--
AOT keyword methods toAny() and toRef()
--FILE--
<?php
function append_text(&$value, string $suffix): void
{
$value .= $suffix;
}
function set_value(&$value, $newValue): void
{
$value = $newValue;
}
function set_named(string $label, &$value): void
{
$value = $label;
}
function main(): void
{
$a = 10;
$b = 4;
var_dump($a->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"

@ -0,0 +1,12 @@
--TEST--
for loop with internal constant bound
--FILE--
<?php
$sum = 0;
for ($i = 0; $i < PHP_FD_SETSIZE; $i++) {
$sum += $i & 1;
}
var_dump($sum);
?>
--EXPECT--
int(512)

@ -0,0 +1,31 @@
--TEST--
object link operator
--FILE--
<?php
function main()
{
$arr = [1, 2, 3];
foreach ($arr as &$value) {
var_dump($value);
}
unset($value);
foreach ($arr as &$value) {
$value += 4;
}
unset($value);
var_dump($arr);
}
?>
--EXPECT--
int(1)
int(2)
int(3)
array(3) {
[0]=>
int(5)
[1]=>
int(6)
[2]=>
int(7)
}

@ -1,5 +1,5 @@
--TEST-- --TEST--
std array: exact class value type std array: class value type accepts subclasses
--FILE-- --FILE--
<?php <?php
class StdArrayClassValue class StdArrayClassValue
@ -33,6 +33,7 @@ function main() {
try { try {
$array[2] = std_array_class_value_mixed(new StdArrayClassValueChild(3)); $array[2] = std_array_class_value_mixed(new StdArrayClassValueChild(3));
var_dump($array[2]->getValue());
} catch (Throwable $e) { } catch (Throwable $e) {
echo $e->getMessage(), "\n"; echo $e->getMessage(), "\n";
} }
@ -41,4 +42,4 @@ function main() {
--EXPECT-- --EXPECT--
int(1) int(1)
int(2) int(2)
The parameter `object` must be instance of class `StdArrayClassValue`, object of `StdArrayClassValueChild` given int(3)

@ -1,5 +1,5 @@
--TEST-- --TEST--
std map: exact class value type std map: class value type accepts subclasses
--FILE-- --FILE--
<?php <?php
class StdMapClassValue class StdMapClassValue
@ -33,6 +33,7 @@ function main() {
try { try {
$map["c"] = std_map_class_value_mixed(new StdMapClassValueChild(3)); $map["c"] = std_map_class_value_mixed(new StdMapClassValueChild(3));
var_dump($map["c"]->getValue());
} catch (Throwable $e) { } catch (Throwable $e) {
echo $e->getMessage(), "\n"; echo $e->getMessage(), "\n";
} }
@ -41,4 +42,4 @@ function main() {
--EXPECT-- --EXPECT--
int(1) int(1)
int(2) int(2)
The parameter `object` must be instance of class `StdMapClassValue`, object of `StdMapClassValueChild` given int(3)

@ -1,5 +1,5 @@
--TEST-- --TEST--
std containers: exact class value type std containers: class value type accepts subclasses
--FILE-- --FILE--
<?php <?php
class StdContainerClassValue class StdContainerClassValue
@ -47,6 +47,7 @@ function main() {
try { try {
$unordered[2] = std_container_class_value_mixed(new StdContainerClassValueChild(5)); $unordered[2] = std_container_class_value_mixed(new StdContainerClassValueChild(5));
var_dump($unordered[2]->getValue());
} catch (Throwable $e) { } catch (Throwable $e) {
echo $e->getMessage(), "\n"; echo $e->getMessage(), "\n";
} }
@ -63,5 +64,5 @@ int(1)
int(2) int(2)
int(3) int(3)
int(4) 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 The parameter `object` must be instance of class `StdContainerClassValue`, object of `StdContainerClassValueOther` given

@ -1,5 +1,5 @@
--TEST-- --TEST--
std vector: exact class value type std vector: class value type accepts subclasses
--FILE-- --FILE--
<?php <?php
class StdVectorClassValue class StdVectorClassValue
@ -33,6 +33,7 @@ function main() {
try { try {
$vector[] = std_vector_class_value_mixed(new StdVectorClassValueChild(3)); $vector[] = std_vector_class_value_mixed(new StdVectorClassValueChild(3));
var_dump($vector[2]->getValue());
} catch (Throwable $e) { } catch (Throwable $e) {
echo $e->getMessage(), "\n"; echo $e->getMessage(), "\n";
} }
@ -41,4 +42,4 @@ function main() {
--EXPECT-- --EXPECT--
int(1) int(1)
int(2) int(2)
The parameter `object` must be instance of class `StdVectorClassValue`, object of `StdVectorClassValueChild` given int(3)

@ -1,5 +1,5 @@
--TEST-- --TEST--
std vector: exact class value type checks typed parameter at runtime std vector: class value type accepts typed parameter subclass at runtime
--FILE-- --FILE--
<?php <?php
class StdVectorRuntimeClassValue class StdVectorRuntimeClassValue
@ -19,6 +19,7 @@ function std_vector_runtime_class_value(StdVectorRuntimeClassValue $value): void
try { try {
$vector[] = $value; $vector[] = $value;
var_dump($vector[0]->value);
} catch (Throwable $e) { } catch (Throwable $e) {
echo $e->getMessage(), "\n"; echo $e->getMessage(), "\n";
} }
@ -29,4 +30,4 @@ function main() {
} }
?> ?>
--EXPECT-- --EXPECT--
The parameter `object` must be instance of class `StdVectorRuntimeClassValue`, object of `StdVectorRuntimeClassValueChild` given int(1)

@ -1,5 +1,5 @@
--TEST-- --TEST--
std vector: exact class value type rejects non-object value std vector: class value type rejects non-object value
--FILE-- --FILE--
<?php <?php
class StdVectorRuntimeScalarValue class StdVectorRuntimeScalarValue

@ -0,0 +1,63 @@
--TEST--
std containers: interface class value type
--FILE--
<?php
interface StdContainerInterfaceValue
{
public function getValue(): int;
}
class StdContainerInterfaceImpl implements StdContainerInterfaceValue
{
public function __construct(private int $value)
{
}
public function getValue(): int
{
return $this->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

@ -0,0 +1,44 @@
--TEST--
std containers: abstract class value type
--FILE--
<?php
abstract class StdContainerAbstractValue
{
abstract public function getValue(): int;
}
class StdContainerAbstractChild extends StdContainerAbstractValue
{
public function __construct(private int $value)
{}
public function getValue(): int
{
return $this->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

@ -2,7 +2,8 @@
abs edge cases: PHP_INT_MIN and -0.0 abs edge cases: PHP_INT_MIN and -0.0
--FILE-- --FILE--
<?php <?php
var_dump(abs(PHP_INT_MIN)); $value = any(PHP_INT_MIN);
var_dump(abs($value));
var_dump(abs(-0.0)); var_dump(abs(-0.0));
var_dump(abs(0)); var_dump(abs(0));
var_dump(abs(-5)); var_dump(abs(-5));

Loading…
Cancel
Save