From 7d1a90a3403e9d79ecc378ff5edbdd1ecf33a47e Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 17 Aug 2026 17:02:51 +0800 Subject: [PATCH] =?UTF-8?q?Native=20Class=20=E6=94=AF=E6=8C=81=20[?= =?UTF-8?q?=E7=AC=AC=E5=85=AD=E9=98=B6=E6=AE=B5]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/NATIVE_CLASS_OBJECT.md | 4 +- phpunit/code/native-class-anonymous.php | 8 + .../native-class-private-method-access.php | 13 ++ .../native-class-protected-method-access.php | 13 ++ phpunit/code/native-class-serialize.php | 10 ++ .../code/native-class-static-signature.php | 10 ++ .../native-class-trait-static-signature.php | 15 ++ phpunit/code/native-class-weak-reference.php | 10 ++ phpunit/src/GenStubVersionFlagsTest.php | 15 ++ .../NativeClass/NativeClassValidationTest.php | 53 ++++++- src/CompilerBase.php | 5 +- src/NativeClass/NativeClassSupportTrait.php | 41 ++++- src/Parser/AssignOpTrait.php | 39 ++++- src/Parser/BinaryOpTrait.php | 3 +- src/gen_stub.php | 11 +- .../constructor-argument-roots.phpt | 62 ++++++++ .../native-class/failed-lifecycle-escape.phpt | 150 ++++++++++++++++++ .../high-precision-properties.phpt | 50 ++++++ .../compiler/native-class/match-identity.phpt | 18 +++ .../native-class/method-visibility.phpt | 56 +++++++ .../property-hook-native-object.phpt | 68 ++++++++ .../native-class/self-parent-signatures.phpt | 62 ++++++++ .../native-class/strict-identity.phpt | 31 ++++ tests/compiler/native-class/this-value.phpt | 54 +++++++ 24 files changed, 784 insertions(+), 17 deletions(-) create mode 100644 phpunit/code/native-class-anonymous.php create mode 100644 phpunit/code/native-class-private-method-access.php create mode 100644 phpunit/code/native-class-protected-method-access.php create mode 100644 phpunit/code/native-class-serialize.php create mode 100644 phpunit/code/native-class-static-signature.php create mode 100644 phpunit/code/native-class-trait-static-signature.php create mode 100644 phpunit/code/native-class-weak-reference.php create mode 100644 phpunit/src/GenStubVersionFlagsTest.php create mode 100644 tests/compiler/native-class/constructor-argument-roots.phpt create mode 100644 tests/compiler/native-class/failed-lifecycle-escape.phpt create mode 100644 tests/compiler/native-class/high-precision-properties.phpt create mode 100644 tests/compiler/native-class/method-visibility.phpt create mode 100644 tests/compiler/native-class/property-hook-native-object.phpt create mode 100644 tests/compiler/native-class/self-parent-signatures.phpt create mode 100644 tests/compiler/native-class/this-value.phpt diff --git a/docs/NATIVE_CLASS_OBJECT.md b/docs/NATIVE_CLASS_OBJECT.md index ed41195b..9004b31f 100644 --- a/docs/NATIVE_CLASS_OBJECT.md +++ b/docs/NATIVE_CLASS_OBJECT.md @@ -858,7 +858,9 @@ Box 不能保存 Native Object。Std Container 不能作为 Native Class 属性 `NativeClass::class` 作为 value type,并保存该类或其 Native 子类。普通 PHP array 仍然不能保存 Native Object。 -Native 元素 Std Container 必须是函数顶层的局部变量。编译器为该局部容器生成与其 +TypePHP 当前的 Std Container 本身就只允许作为函数内的局部变量,不允许作为 +global/static,因此不存在需要为 Native 元素另外设计的长期容器所有权。Native 元素 +Std Container 进一步要求它是函数顶层的局部变量。编译器为该局部容器生成与其 词法生命周期一致的 `NativeContainerRootFrame`;因此它不能保存到 global/static、 Zend 或 Native 属性、PHP array,也不能被返回、取引用、捕获进 Closure/arrow function,或通过 `toArray()`/`toAny()` 等方式转换。上述行为都会让保存裸指针的 diff --git a/phpunit/code/native-class-anonymous.php b/phpunit/code/native-class-anonymous.php new file mode 100644 index 00000000..d2b267ec --- /dev/null +++ b/phpunit/code/native-class-anonymous.php @@ -0,0 +1,8 @@ +hidden(); +} diff --git a/phpunit/code/native-class-protected-method-access.php b/phpunit/code/native-class-protected-method-access.php new file mode 100644 index 00000000..2c79b462 --- /dev/null +++ b/phpunit/code/native-class-protected-method-access.php @@ -0,0 +1,13 @@ +hidden(); +} diff --git a/phpunit/code/native-class-serialize.php b/phpunit/code/native-class-serialize.php new file mode 100644 index 00000000..6699106f --- /dev/null +++ b/phpunit/code/native-class-serialize.php @@ -0,0 +1,10 @@ +addForVersionsAbove('ZEND_ACC_STATIC', PHP_70_VERSION_ID); + + self::assertSame( + 'ZEND_ACC_PRIVATE|ZEND_ACC_STATIC', + $flags->generateVersionDependentFlagCode('%s', null), + ); + } +} diff --git a/phpunit/src/NativeClass/NativeClassValidationTest.php b/phpunit/src/NativeClass/NativeClassValidationTest.php index ad21eeed..821e6e6a 100644 --- a/phpunit/src/NativeClass/NativeClassValidationTest.php +++ b/phpunit/src/NativeClass/NativeClassValidationTest.php @@ -46,6 +46,13 @@ final class NativeClassValidationTest extends \BaseTest $this->compile('native-class-attribute-enum.php'); } + public function testRejectsNativeAttributeOnAnonymousClass(): void + { + $this->expectException(\TypePhp\Exception\SyntaxError::class); + $this->expectExceptionMessage('Native can only be applied to named classes'); + $this->compile('native-class-anonymous.php'); + } + public function testRejectsUntypedProperty(): void { $this->expectException(TestError::class); @@ -435,6 +442,24 @@ final class NativeClassValidationTest extends \BaseTest $this->compile('native-class-json-encode.php'); } + /** + * @dataProvider nativeZendObjectFacilityProvider + */ + public function testRejectsNativeObjectPassedToZendObjectFacilities(string $fixture): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native objects cannot cross a dynamic PHP/ZendVM call boundary'); + $this->compile($fixture); + } + + public static function nativeZendObjectFacilityProvider(): array + { + return [ + ['native-class-serialize.php'], + ['native-class-weak-reference.php'], + ]; + } + public function testRejectsNativeObjectFromUntypedReturn(): void { $this->expectException(TestError::class); @@ -760,10 +785,17 @@ final class NativeClassValidationTest extends \BaseTest public function testRejectsLateStaticNativeMethodSignature(): void { $this->expectException(TestError::class); - $this->expectExceptionMessage('Native classes do not support late static binding in parameter or return types'); + $this->expectExceptionMessage('Native classes do not support late static binding in return types'); $this->compile('native-class-static-signature.php'); } + public function testRejectsLateStaticSignatureInjectedIntoNativeClassByTrait(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Native classes do not support late static binding in return types'); + $this->compile('native-class-trait-static-signature.php'); + } + public function testRejectsInaccessibleNativeClassConstant(): void { $this->expectException(TestError::class); @@ -771,6 +803,25 @@ final class NativeClassValidationTest extends \BaseTest $this->compile('native-class-private-constant-access.php'); } + /** + * @dataProvider inaccessibleNativeMethodProvider + */ + public function testRejectsInaccessibleNativeMethods(string $fixture): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Method'); + $this->expectExceptionMessage('is not accessible'); + $this->compile($fixture); + } + + public static function inaccessibleNativeMethodProvider(): array + { + return [ + ['native-class-private-method-access.php'], + ['native-class-protected-method-access.php'], + ]; + } + public function testRejectsNativeObjectCastToZendObject(): void { $this->expectException(TestError::class); diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 2c2f5d7c..443efba4 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1170,7 +1170,8 @@ class CompilerBase implements PropertyAccessContext protected function parseExprAsValue(NodeAbstract $expr): string { - return $this->wrapVoidExprAsNull($expr, $this->parseExpr($expr)); + $value = $this->wrapVoidExprAsNull($expr, $this->parseExpr($expr)); + return $this->normalizeNativeObjectValueExpr($expr, $value); } /** @@ -1886,7 +1887,7 @@ class CompilerBase implements PropertyAccessContext } } - return $this->parseIdentifier($expr); + return $this->normalizeNativeObjectValueExpr($expr, $this->parseIdentifier($expr)); } protected function detectClassOfExpr(NodeAbstract $expr): string diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php index cc3b97d6..fe35b2e6 100644 --- a/src/NativeClass/NativeClassSupportTrait.php +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -556,6 +556,12 @@ trait NativeClassSupportTrait Node\Stmt\Function_|Node\Stmt\ClassMethod $node, FunctionDef $function, ): void { + if ($this->classDef?->nativeObject && $function->returnTypeKeyword === 'static') { + $this->fatalError( + $node, + 'Native classes do not support late static binding in return types', + ); + } if ($this->isNativeObjectClass($function->returnClass) && $function->returnsByRef) { $this->fatalError($node, 'Native objects cannot be returned by reference'); } @@ -825,6 +831,25 @@ trait NativeClassSupportTrait return 'php::nativeDeref(' . $name . ', "' . addslashes($class) . '")'; } + /** + * Native method bodies receive `this_` by C++ reference so direct member + * access remains zero-cost. In a PHP value context, however, `$this` is an + * object handle and must therefore become the address of that reference. + * Keep this conversion out of parseIdentifier(): receiver contexts need + * the reference itself, while assignments, returns, arguments and + * comparisons need the typed pointer. + */ + protected function normalizeNativeObjectValueExpr(NodeAbstract $expr, string $value): string + { + if ($this->classDef?->nativeObject + && $this->isVarExpr($expr) + && $this->parseVariable($expr) === 'this_' + ) { + return '&this_'; + } + return $value; + } + protected function getNativeObjectMemberReceiver(string $name): string { return $this->getNativeObjectReceiver($name) . '.'; @@ -1478,12 +1503,18 @@ trait NativeClassSupportTrait } if ($destructors !== []) { $code .= 'static void ' . $prefix . '_finalize(void *object) {' . PHP_EOL; - $code .= ' php::NativeFinalizerChain chain;' . PHP_EOL; - foreach ($destructors as [$destructor, $destructorCpp]) { - $code .= ' chain.run([&] { ' . $destructor - . '(*static_cast<' . $destructorCpp . ' *>(object)); });' . PHP_EOL; + if (count($destructors) === 1) { + [$destructor, $destructorCpp] = $destructors[0]; + $code .= ' ' . $destructor + . '(*static_cast<' . $destructorCpp . ' *>(object));' . PHP_EOL; + } else { + $code .= ' php::NativeFinalizerChain chain;' . PHP_EOL; + foreach ($destructors as [$destructor, $destructorCpp]) { + $code .= ' chain.run([&] { ' . $destructor + . '(*static_cast<' . $destructorCpp . ' *>(object)); });' . PHP_EOL; + } + $code .= ' chain.rethrow();' . PHP_EOL; } - $code .= ' chain.rethrow();' . PHP_EOL; $code .= '}' . PHP_EOL; } $code .= 'static void ' . $prefix . '_destroy(void *object) noexcept {' . PHP_EOL; diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index fea5083d..ac534f83 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -575,15 +575,48 @@ trait AssignOpTrait ?PropertyWriteTarget $target, string $setter, ): string { + $property = $this->getNativePropertyDef($left); + $nativeObjectClass = $property !== null + && $property->type === Type::OBJECT + && $this->isNativeObjectClass($property->class) + ? $property->class + : ''; if ($target !== null) { $this->assertCanAssignPropertyWrite($target, $right); } - $rightExpr = $this->parseExprAsValue($right); - if ($target !== null) { + if ($nativeObjectClass !== '') { + if ($this->isNull($right)) { + if (!$property->nullable) { + $this->fatalError($right, "Cannot assign null to native property `{$nativeObjectClass}`"); + } + $rightExpr = 'nullptr'; + } else { + $rightClass = $this->detectClassOfExpr($right); + if ($rightClass === '' + || !$this->isNativeObjectClass($rightClass) + || !$this->isObjectClassStaticallyAssignableTo($rightClass, $nativeObjectClass) + ) { + $this->fatalError($right, "Cannot assign value to native property of type `{$nativeObjectClass}`"); + } + $rightExpr = $this->parseExprAsValue($right); + } + } else { + $rightExpr = $this->parseExprAsValue($right); + } + if ($target !== null && $nativeObjectClass === '') { $rightExpr = $this->wrapPropertyWriteTypeCheck($target, $right, $rightExpr); } $tmp = $this->genTmpVarName(); - $this->addLocalVar($tmp, Type::VAR); + if ($nativeObjectClass !== '') { + // The synthesized value variable is also the assignment result and + // the setter argument. Preserve its exact Native type so argument + // validation and NativeRootFrame generation do not see a mixed + // Zend value at this compiler-created boundary. + $this->addLocalVar($tmp, $this->getNativeObjectPointerType($nativeObjectClass)); + $this->addNativeObject($tmp, $nativeObjectClass); + } else { + $this->addLocalVar($tmp, Type::VAR); + } $call = $this->emitPropertyHookSetterCall($left, $setter, new Expr\Variable($tmp)); return '((' . $tmp . ' = ' . $rightExpr . ', ' . $call . '), ' . $tmp . ')'; } diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index cef4b1a4..5778b37e 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -525,7 +525,8 @@ trait BinaryOpTrait { $this->assertExprCanBeUsedAsValue($expr, 'operand'); if (!$this->shouldMaterializeOrderedOperand($expr)) { - return $numeric ? $this->parseNumericIdentifier($expr) : $this->parseIdentifier($expr); + $value = $numeric ? $this->parseNumericIdentifier($expr) : $this->parseIdentifier($expr); + return $this->normalizeNativeObjectValueExpr($expr, $value); } [$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr); diff --git a/src/gen_stub.php b/src/gen_stub.php index 273e0f63..f8c89bfd 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -1261,12 +1261,15 @@ class VersionFlags { } public function addForVersionsAbove(string $flag, int $minimumVersionId): void { - $write = false; - foreach (ALL_PHP_VERSION_IDS as $version) { - if ($version === $minimumVersionId || $write === true) { + // The supported-version table does not necessarily contain the + // PHP release that originally introduced a flag. For example, + // TypePHP currently starts at PHP 8.4, while ZEND_ACC_STATIC was + // introduced much earlier. Compare ranges instead of waiting for + // an exact table entry, otherwise established flags silently + // disappear when the minimum supported PHP version is raised. + if ($version >= $minimumVersionId) { $this->flagsByVersion[$version][] = $flag; - $write = true; } } } diff --git a/tests/compiler/native-class/constructor-argument-roots.phpt b/tests/compiler/native-class/constructor-argument-roots.phpt new file mode 100644 index 00000000..e80b7c12 --- /dev/null +++ b/tests/compiler/native-class/constructor-argument-roots.phpt @@ -0,0 +1,62 @@ +--TEST-- +Native class: constructor arguments are evaluated left-to-right and rooted +--FILE-- +name = $name; + } +} + +#[Native] +class NativeConstructorPressure +{ + public int $value; +} + +#[Native] +class NativeConstructorPair +{ + public NativeConstructorArgument $first; + public NativeConstructorArgument $second; + + public function __construct( + NativeConstructorArgument $first, + NativeConstructorArgument $second, + ) { + $this->first = $first; + $this->second = $second; + } +} + +function makeNativeConstructorArgument(string $name): NativeConstructorArgument +{ + echo 'make:', $name, PHP_EOL; + if ($name === 'B') { + for ($i = 0; $i < 300000; $i++) { + $filler = new NativeConstructorPressure(); + } + } + return new NativeConstructorArgument($name); +} + +function main(): void +{ + $pair = new NativeConstructorPair( + makeNativeConstructorArgument('A'), + makeNativeConstructorArgument('B'), + ); + echo $pair->first->name, ':', $pair->second->name, PHP_EOL; +} + +?> +--EXPECT-- +make:A +make:B +A:B diff --git a/tests/compiler/native-class/failed-lifecycle-escape.phpt b/tests/compiler/native-class/failed-lifecycle-escape.phpt new file mode 100644 index 00000000..2cd7e61c --- /dev/null +++ b/tests/compiler/native-class/failed-lifecycle-escape.phpt @@ -0,0 +1,150 @@ +--TEST-- +Native class: objects published before constructor or clone failure never become dangling +--FILE-- +name; + } +} + +#[Native] +class NativeFailedConstructionEscape +{ + public object $probe; + public int $value = 42; + + public function __construct() + { + global $failedConstruction; + $this->probe = new NativeFailedLifecycleProbe('constructor field'); + $failedConstruction = $this; + throw new RuntimeException('constructor failed'); + } + + public function __destruct() + { + global $finalized; + $finalized[] = 'constructor object'; + } +} + +#[Native] +class NativeFailedCloneEscape +{ + public object $probe; + public string $kind = 'source'; + + public function __construct() + { + $this->probe = new NativeFailedLifecycleProbe('source field'); + } + + public function __clone(): void + { + global $failedClone; + $this->kind = 'clone'; + $this->probe = new NativeFailedLifecycleProbe('clone field'); + $failedClone = $this; + throw new RuntimeException('clone failed'); + } + + public function __destruct() + { + global $finalized; + $finalized[] = $this->kind; + } +} + +#[Native] +class NativeFailedLifecyclePressure +{ + public int $value; +} + +function forceNativeCollection(): void +{ + for ($i = 0; $i < 400000; $i++) { + $filler = new NativeFailedLifecyclePressure(); + } +} + +function main(): void +{ + global $failedConstruction, $failedClone, $released, $finalized; + $released = []; + $finalized = []; + + try { + $unused = new NativeFailedConstructionEscape(); + } catch (RuntimeException $error) { + echo $error->getMessage(), "\n"; + } + var_dump($failedConstruction->value, $failedConstruction->probe->name); + $failedConstruction = null; + forceNativeCollection(); + var_dump($released, $finalized); + + $source = new NativeFailedCloneEscape(); + try { + $unusedClone = clone $source; + } catch (RuntimeException $error) { + echo $error->getMessage(), "\n"; + } + var_dump($failedClone->kind, $failedClone === $source, $failedClone->probe->name); + $failedClone = null; + forceNativeCollection(); + var_dump($released, $finalized); + + $source = null; + forceNativeCollection(); + var_dump($released, $finalized); +} + +?> +--EXPECT-- +constructor failed +int(42) +string(17) "constructor field" +array(1) { + [0]=> + string(17) "constructor field" +} +array(0) { +} +clone failed +string(5) "clone" +bool(false) +string(11) "clone field" +array(2) { + [0]=> + string(17) "constructor field" + [1]=> + string(11) "clone field" +} +array(1) { + [0]=> + string(5) "clone" +} +array(3) { + [0]=> + string(17) "constructor field" + [1]=> + string(11) "clone field" + [2]=> + string(12) "source field" +} +array(2) { + [0]=> + string(5) "clone" + [1]=> + string(6) "source" +} diff --git a/tests/compiler/native-class/high-precision-properties.phpt b/tests/compiler/native-class/high-precision-properties.phpt new file mode 100644 index 00000000..7e63f1a5 --- /dev/null +++ b/tests/compiler/native-class/high-precision-properties.phpt @@ -0,0 +1,50 @@ +--TEST-- +Native class: BigInt, BigFloat and Decimal properties retain typed value semantics +--FILE-- +integer, $values->floating, $values->decimal); + + $values->integer = std::bigInt('123456789012345678901234567890'); + $values->floating = std::bigFloat('3.141592653589793238462643383279'); + $values->decimal = std::decimal('99.125'); + + createNativePressure(); + + echo ($values->integer + 10)->toString(), "\n"; + echo $values->floating->toString(), "\n"; + echo ($values->decimal * 2)->toString(), "\n"; +} + +?> +--EXPECT-- +NULL +NULL +NULL +123456789012345678901234567900 +3.141592653589793238462643383279 +198.250 diff --git a/tests/compiler/native-class/match-identity.phpt b/tests/compiler/native-class/match-identity.phpt index 3082fd2d..78cacf47 100644 --- a/tests/compiler/native-class/match-identity.phpt +++ b/tests/compiler/native-class/match-identity.phpt @@ -27,6 +27,19 @@ function scalarMatchValue(): int return 42; } +function temporaryMatchSubject(): NativeMatchValue +{ + return new NativeMatchValue(); +} + +function pressuredMatchValue(): NativeMatchValue +{ + for ($i = 0; $i < 300000; $i++) { + $filler = new NativeMatchValue(); + } + return new NativeMatchValue(); +} + function choose(?NativeMatchValue $subject, NativeMatchValue $same, NativeMatchValue $other): string { return match ($subject) { @@ -48,6 +61,10 @@ function main(): void scalarMatchValue() => 'scalar', $value => 'identity', }); + var_dump(match (temporaryMatchSubject()) { + pressuredMatchValue() => 'reused', + default => 'distinct', + }); } ?> @@ -58,3 +75,4 @@ subject unrelated scalar string(8) "identity" +string(8) "distinct" diff --git a/tests/compiler/native-class/method-visibility.phpt b/tests/compiler/native-class/method-visibility.phpt new file mode 100644 index 00000000..e2db2a6d --- /dev/null +++ b/tests/compiler/native-class/method-visibility.phpt @@ -0,0 +1,56 @@ +--TEST-- +Native class: method visibility is resolved at compile time across inheritance +--FILE-- +baseLabel(); + } +} + +#[Native] +class NativeVisibilityChild extends NativeVisibilityBase +{ + private function childLabel(): string + { + return 'child-private'; + } + + public function callChildPrivate(): string + { + return $this->childLabel(); + } + + public function callProtected(): string + { + return $this->protectedLabel(); + } +} + +function main(): void +{ + $value = new NativeVisibilityChild(); + echo $value->callBasePrivate(), PHP_EOL; + echo $value->callChildPrivate(), PHP_EOL; + echo $value->callProtected(), PHP_EOL; +} + +?> +--EXPECT-- +base-private +child-private +base-protected diff --git a/tests/compiler/native-class/property-hook-native-object.phpt b/tests/compiler/native-class/property-hook-native-object.phpt new file mode 100644 index 00000000..80554045 --- /dev/null +++ b/tests/compiler/native-class/property-hook-native-object.phpt @@ -0,0 +1,68 @@ +--TEST-- +Native class: property hooks preserve native pointer types and temporary roots +--FILE-- +name = $name; + } +} + +#[Native] +class NativeHookObjectPressure +{ + public int $value; +} + +#[Native] +class NativeHookObjectOwner +{ + private ?NativeHookObjectValue $stored; + + public ?NativeHookObjectValue $value { + get { + return $this->stored; + } + set(?NativeHookObjectValue $value) { + $this->stored = $value; + } + } +} + +function makeNativeHookValueAfterPressure(string $name): NativeHookObjectValue +{ + for ($i = 0; $i < 300000; $i++) { + $filler = new NativeHookObjectPressure(); + } + return new NativeHookObjectValue($name); +} + +function consumeNativeHookValues( + NativeHookObjectValue $first, + NativeHookObjectValue $second, +): void { + echo $first->name, ':', $second->name, PHP_EOL; +} + +function main(): void +{ + $owner = new NativeHookObjectOwner(); + $owner->value = new NativeHookObjectValue('first'); + consumeNativeHookValues( + $owner->value, + makeNativeHookValueAfterPressure('second'), + ); + $owner->value = null; + echo $owner->value === null ? "NULL\n" : "not-null\n"; +} + +?> +--EXPECT-- +first:second +NULL diff --git a/tests/compiler/native-class/self-parent-signatures.phpt b/tests/compiler/native-class/self-parent-signatures.phpt new file mode 100644 index 00000000..81659cc7 --- /dev/null +++ b/tests/compiler/native-class/self-parent-signatures.phpt @@ -0,0 +1,62 @@ +--TEST-- +Native class: self and parent signatures preserve typed pointer semantics +--FILE-- +peer = $base; + $child->parentPeer = $child; + + var_dump( + $base->identity($base) === $base, + $base->nullable(null) === null, + $child->childIdentity($child) === $child, + $child->parentIdentity($child) === $child, + $base->peer === $base, + $child->parentPeer === $child, + ); +} + +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/tests/compiler/native-class/strict-identity.phpt b/tests/compiler/native-class/strict-identity.phpt index f2a05599..03023e83 100644 --- a/tests/compiler/native-class/strict-identity.phpt +++ b/tests/compiler/native-class/strict-identity.phpt @@ -6,6 +6,12 @@ Native class: strict identity never coerces raw pointers to PHP values #[Native] class NativeStrictIdentity {} +#[Native] +class NativeStrictIdentityBase {} + +#[Native] +class NativeStrictIdentityChild extends NativeStrictIdentityBase {} + function nativeIdentityOperand(NativeStrictIdentity $value): NativeStrictIdentity { echo "native\n"; @@ -18,6 +24,24 @@ function zendIdentityOperand(): bool return true; } +function makeIdentityOperand(): NativeStrictIdentity +{ + return new NativeStrictIdentity(); +} + +function makeIdentityOperandAfterPressure(): NativeStrictIdentity +{ + for ($i = 0; $i < 300000; $i++) { + $filler = new NativeStrictIdentity(); + } + return new NativeStrictIdentity(); +} + +function identityAsBase(NativeStrictIdentityBase $value): NativeStrictIdentityBase +{ + return $value; +} + function main(): void { $value = new NativeStrictIdentity(); @@ -30,6 +54,10 @@ function main(): void var_dump($value !== true); var_dump(nativeIdentityOperand($value) === zendIdentityOperand()); var_dump(zendIdentityOperand() === nativeIdentityOperand($value)); + var_dump(makeIdentityOperand() === makeIdentityOperandAfterPressure()); + $child = new NativeStrictIdentityChild(); + $base = identityAsBase($child); + var_dump($base === $child, $child === $base); } ?> @@ -44,3 +72,6 @@ bool(false) zend native bool(false) +bool(false) +bool(true) +bool(true) diff --git a/tests/compiler/native-class/this-value.phpt b/tests/compiler/native-class/this-value.phpt new file mode 100644 index 00000000..488347db --- /dev/null +++ b/tests/compiler/native-class/this-value.phpt @@ -0,0 +1,54 @@ +--TEST-- +Native class: $this is a native object pointer in value contexts +--FILE-- +value = $value; + } + + public function identity(): self + { + return $this; + } + + public function sameAs(NativeThisValue $other): bool + { + $alias = $this; + return $alias === $other; + } + + public function passToFunction(): int + { + return readNativeThisValue($this); + } +} + +function readNativeThisValue(NativeThisValue $value): int +{ + return $value->value; +} + +function main(): void +{ + $value = new NativeThisValue(42); + $other = new NativeThisValue(42); + + var_dump($value->identity() === $value); + var_dump($value->sameAs($value)); + var_dump($value->sameAs($other)); + var_dump($value->passToFunction()); +} + +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) +int(42)