parent
af0b6f9582
commit
981b8f54d7
91 changed files with 4887 additions and 61 deletions
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,15 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeClosureCapture |
||||
{ |
||||
public int $value = 1; |
||||
} |
||||
|
||||
function makeNativeCapturingClosure(): Closure |
||||
{ |
||||
$native = new NativeClosureCapture(); |
||||
return static function () use ($native): int { |
||||
return 1; |
||||
}; |
||||
} |
||||
@ -0,0 +1,14 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeClosureParameter |
||||
{ |
||||
public int $value = 1; |
||||
} |
||||
|
||||
function makeNativeParameterClosure(): Closure |
||||
{ |
||||
return static function (NativeClosureParameter $native): int { |
||||
return $native->value; |
||||
}; |
||||
} |
||||
@ -0,0 +1,14 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeClosureReturn |
||||
{ |
||||
public int $value = 1; |
||||
} |
||||
|
||||
function makeNativeReturningClosure(): Closure |
||||
{ |
||||
return static function () { |
||||
return new NativeClosureReturn(); |
||||
}; |
||||
} |
||||
@ -0,0 +1,10 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeDynamicMagicMethod |
||||
{ |
||||
public function __get(string $name): mixed |
||||
{ |
||||
return null; |
||||
} |
||||
} |
||||
@ -0,0 +1,10 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeDynamicStaticMagicMethod |
||||
{ |
||||
public static function __callStatic(string $name, array $arguments): mixed |
||||
{ |
||||
return null; |
||||
} |
||||
} |
||||
@ -0,0 +1,13 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeInstanceofTarget |
||||
{ |
||||
public int $value = 0; |
||||
} |
||||
|
||||
function testNativeInstanceof(string $class): bool |
||||
{ |
||||
$value = new NativeInstanceofTarget(); |
||||
return $value instanceof $class; |
||||
} |
||||
@ -0,0 +1,25 @@ |
||||
<?php |
||||
|
||||
interface NativeInterfaceArgumentContract |
||||
{ |
||||
public function value(): int; |
||||
} |
||||
|
||||
#[Native] |
||||
class NativeInterfaceArgumentValue implements NativeInterfaceArgumentContract |
||||
{ |
||||
public function value(): int |
||||
{ |
||||
return 1; |
||||
} |
||||
} |
||||
|
||||
function consumeNativeInterfaceArgument(NativeInterfaceArgumentContract $value): int |
||||
{ |
||||
return $value->value(); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
consumeNativeInterfaceArgument(new NativeInterfaceArgumentValue()); |
||||
} |
||||
@ -0,0 +1,22 @@ |
||||
<?php |
||||
|
||||
interface NativeInterfaceAssignmentContract |
||||
{ |
||||
public function value(): int; |
||||
} |
||||
|
||||
#[Native] |
||||
class NativeInterfaceAssignmentValue implements NativeInterfaceAssignmentContract |
||||
{ |
||||
public function value(): int |
||||
{ |
||||
return 1; |
||||
} |
||||
} |
||||
|
||||
function replaceNativeInterfaceAssignment( |
||||
NativeInterfaceAssignmentContract $target, |
||||
NativeInterfaceAssignmentValue $value, |
||||
): void { |
||||
$target = $value; |
||||
} |
||||
@ -0,0 +1,27 @@ |
||||
<?php |
||||
|
||||
interface NativeInterfacePropertyContract |
||||
{ |
||||
public function value(): int; |
||||
} |
||||
|
||||
#[Native] |
||||
class NativeInterfacePropertyValue implements NativeInterfacePropertyContract |
||||
{ |
||||
public function value(): int |
||||
{ |
||||
return 1; |
||||
} |
||||
} |
||||
|
||||
#[Native] |
||||
class NativeInterfacePropertyHolder |
||||
{ |
||||
public NativeInterfacePropertyContract $value; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$holder = new NativeInterfacePropertyHolder(); |
||||
$holder->value = new NativeInterfacePropertyValue(); |
||||
} |
||||
@ -0,0 +1,20 @@ |
||||
<?php |
||||
|
||||
interface NativeInterfaceReturnContract |
||||
{ |
||||
public function value(): int; |
||||
} |
||||
|
||||
#[Native] |
||||
class NativeInterfaceReturnValue implements NativeInterfaceReturnContract |
||||
{ |
||||
public function value(): int |
||||
{ |
||||
return 1; |
||||
} |
||||
} |
||||
|
||||
function makeNativeInterfaceReturn(): NativeInterfaceReturnContract |
||||
{ |
||||
return new NativeInterfaceReturnValue(); |
||||
} |
||||
@ -0,0 +1,6 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeMissingCountable implements Countable |
||||
{ |
||||
} |
||||
@ -0,0 +1,23 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeNarrowArrayAccess implements ArrayAccess |
||||
{ |
||||
public function offsetExists(string $offset): bool |
||||
{ |
||||
return false; |
||||
} |
||||
|
||||
public function offsetGet(mixed $offset): mixed |
||||
{ |
||||
return null; |
||||
} |
||||
|
||||
public function offsetSet(mixed $offset, mixed $value): void |
||||
{ |
||||
} |
||||
|
||||
public function offsetUnset(mixed $offset): void |
||||
{ |
||||
} |
||||
} |
||||
@ -0,0 +1,10 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeProtectedCountable implements Countable |
||||
{ |
||||
protected function count(): int |
||||
{ |
||||
return 1; |
||||
} |
||||
} |
||||
@ -0,0 +1,16 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeJsonValue |
||||
{ |
||||
public function toArray(): array |
||||
{ |
||||
return []; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$value = new NativeJsonValue(); |
||||
json_encode($value); |
||||
} |
||||
@ -0,0 +1,13 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class MissingNativeConversion |
||||
{ |
||||
public int $value; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$value = new MissingNativeConversion(); |
||||
$value->toInt(); |
||||
} |
||||
@ -0,0 +1,16 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class InvalidNativeConversion |
||||
{ |
||||
public function toArray(): string |
||||
{ |
||||
return ''; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$value = new InvalidNativeConversion(); |
||||
$value->toArray(); |
||||
} |
||||
@ -0,0 +1,11 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeMixedReturn {} |
||||
|
||||
function makeNativeMixed(): mixed |
||||
{ |
||||
return new NativeMixedReturn(); |
||||
} |
||||
|
||||
function main(): void {} |
||||
@ -0,0 +1,12 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeArrayElement |
||||
{ |
||||
public int $value = 1; |
||||
} |
||||
|
||||
function storeNativeInArray(): array |
||||
{ |
||||
return [new NativeArrayElement()]; |
||||
} |
||||
@ -0,0 +1,13 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativePropertyValue |
||||
{ |
||||
public int $value = 1; |
||||
} |
||||
|
||||
function storeNativeInPhpProperty(): void |
||||
{ |
||||
$phpObject = new stdClass(); |
||||
$phpObject->value = new NativePropertyValue(); |
||||
} |
||||
@ -0,0 +1,7 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeWithReadonlyProperty |
||||
{ |
||||
public readonly int $value; |
||||
} |
||||
@ -0,0 +1,9 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeWithStaticMethod |
||||
{ |
||||
public static function create(): void |
||||
{ |
||||
} |
||||
} |
||||
@ -0,0 +1,7 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeStaticProperty |
||||
{ |
||||
public static int $value = 1; |
||||
} |
||||
@ -0,0 +1,7 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeWithStdContainer |
||||
{ |
||||
public std\vector $values; |
||||
} |
||||
@ -0,0 +1,15 @@ |
||||
<?php |
||||
|
||||
trait NativeDynamicMagicTrait |
||||
{ |
||||
public function __serialize(): array |
||||
{ |
||||
return []; |
||||
} |
||||
} |
||||
|
||||
#[Native] |
||||
class NativeTraitDynamicMagicMethod |
||||
{ |
||||
use NativeDynamicMagicTrait; |
||||
} |
||||
@ -0,0 +1,15 @@ |
||||
<?php |
||||
|
||||
trait NativeStaticMethodTrait |
||||
{ |
||||
public static function value(): int |
||||
{ |
||||
return 1; |
||||
} |
||||
} |
||||
|
||||
#[Native] |
||||
class NativeTraitStaticMethod |
||||
{ |
||||
use NativeStaticMethodTrait; |
||||
} |
||||
@ -0,0 +1,11 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeUnionMember |
||||
{ |
||||
public int $value = 1; |
||||
} |
||||
|
||||
function invalidNativeUnion(NativeUnionMember|string $value): void |
||||
{ |
||||
} |
||||
@ -0,0 +1,7 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeUntypedProperty |
||||
{ |
||||
public $value; |
||||
} |
||||
@ -0,0 +1,11 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeUntypedReturn {} |
||||
|
||||
function makeNativeUntyped() |
||||
{ |
||||
return new NativeUntypedReturn(); |
||||
} |
||||
|
||||
function main(): void {} |
||||
@ -0,0 +1,14 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeConstructorArgument |
||||
{ |
||||
public int $value = 1; |
||||
} |
||||
|
||||
class ZendBackedConstructor |
||||
{ |
||||
public function __construct(NativeConstructorArgument $native) |
||||
{ |
||||
} |
||||
} |
||||
@ -0,0 +1,6 @@ |
||||
<?php |
||||
|
||||
class ZendBackedParent {} |
||||
|
||||
#[Native] |
||||
class NativeInvalidChild extends ZendBackedParent {} |
||||
@ -0,0 +1,12 @@ |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativePropertyType |
||||
{ |
||||
public int $value = 1; |
||||
} |
||||
|
||||
class ZendObjectWithNativeProperty |
||||
{ |
||||
public NativePropertyType $value; |
||||
} |
||||
@ -0,0 +1,38 @@ |
||||
<?php |
||||
|
||||
namespace TypePhp\Tests\NativeClass; |
||||
|
||||
use PhpParser\Node\Stmt\Class_; |
||||
use PhpParser\NodeTraverser; |
||||
use PhpParser\NodeVisitor\NameResolver; |
||||
use PhpParser\ParserFactory; |
||||
use PHPUnit\Framework\TestCase; |
||||
use TypePhp\Transform\NativeClassAttributeLowering; |
||||
|
||||
final class NativeClassAttributeLoweringTest extends TestCase |
||||
{ |
||||
public function testConsumesNativeAttributeAndMarksNamedClass(): void |
||||
{ |
||||
$parser = (new ParserFactory())->createForNewestSupportedVersion(); |
||||
$nodes = $parser->parse('<?php #[Native] class Point {}');
|
||||
$traverser = new NodeTraverser(); |
||||
$traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); |
||||
$traverser->addVisitor(new NativeClassAttributeLowering()); |
||||
$nodes = $traverser->traverse($nodes); |
||||
|
||||
$this->assertInstanceOf(Class_::class, $nodes[0]); |
||||
$this->assertTrue(NativeClassAttributeLowering::isNative($nodes[0])); |
||||
$this->assertSame([], $nodes[0]->attrGroups); |
||||
} |
||||
|
||||
public function testLeavesOrdinaryClassUnmarked(): void |
||||
{ |
||||
$parser = (new ParserFactory())->createForNewestSupportedVersion(); |
||||
$nodes = $parser->parse('<?php class Point {}'); |
||||
$traverser = new NodeTraverser(); |
||||
$traverser->addVisitor(new NativeClassAttributeLowering()); |
||||
$nodes = $traverser->traverse($nodes); |
||||
|
||||
$this->assertFalse(NativeClassAttributeLowering::isNative($nodes[0])); |
||||
} |
||||
} |
||||
@ -0,0 +1,246 @@ |
||||
<?php |
||||
|
||||
namespace TypePhp\Tests\NativeClass; |
||||
|
||||
use TypePhp\Exception\TestError; |
||||
|
||||
final class NativeClassValidationTest extends \BaseTest |
||||
{ |
||||
public function testRejectsUntypedProperty(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native class properties must declare a type'); |
||||
$this->compile('native-class-untyped-property.php'); |
||||
} |
||||
|
||||
public function testRejectsStaticProperty(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native class static properties are not supported'); |
||||
$this->compile('native-class-static-property.php'); |
||||
} |
||||
|
||||
public function testRejectsInheritanceAcrossObjectModels(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native and ZendVM-backed classes cannot inherit from each other'); |
||||
$this->compile('native-class-zend-inheritance.php'); |
||||
} |
||||
|
||||
public function testRejectsStaticMethod(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native class static methods are not supported'); |
||||
$this->compile('native-class-static-method.php'); |
||||
} |
||||
|
||||
public function testRejectsReadonlyPropertyUntilNativeWriteStateIsImplemented(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native class readonly properties are not supported'); |
||||
$this->compile('native-class-readonly-property.php'); |
||||
} |
||||
|
||||
public function testRejectsStdContainerProperty(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native class properties cannot use Std Container types'); |
||||
$this->compile('native-class-std-container-property.php'); |
||||
} |
||||
|
||||
public function testRejectsDynamicInstanceofBecauseNativeClassesHaveNoRuntimeTypeLookup(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Dynamic instanceof is not supported for native objects'); |
||||
$this->compile('native-class-instanceof.php'); |
||||
} |
||||
|
||||
public function testRejectsNativeObjectStoredInPhpArray(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native objects cannot be stored in PHP arrays'); |
||||
$this->compile('native-class-php-array.php'); |
||||
} |
||||
|
||||
public function testRejectsNativeObjectStoredInPhpObjectProperty(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native objects cannot be stored in PHP arrays, PHP object properties'); |
||||
$this->compile('native-class-php-property.php'); |
||||
} |
||||
|
||||
public function testRejectsNativeTypedPropertyOnZendObject(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native object types can only be used as properties of native classes'); |
||||
$this->compile('native-class-zend-native-property.php'); |
||||
} |
||||
|
||||
public function testRejectsNativeObjectCapturedByClosure(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native objects cannot be captured by Zend closures'); |
||||
$this->compile('native-class-closure-capture.php'); |
||||
} |
||||
|
||||
public function testRejectsNativeObjectClosureParameter(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Zend closures cannot declare native object parameters or return types'); |
||||
$this->compile('native-class-closure-parameter.php'); |
||||
} |
||||
|
||||
public function testRejectsNativeObjectReturnedByUntypedClosure(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Zend closures cannot return native objects'); |
||||
$this->compile('native-class-closure-return.php'); |
||||
} |
||||
|
||||
public function testRejectsNativeObjectParameterOnZendConstructor(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Zend-backed constructors cannot accept or return native objects'); |
||||
$this->compile('native-class-zend-constructor.php'); |
||||
} |
||||
|
||||
public function testRejectsUnsupportedNativeObjectUnion(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native object types cannot be combined with other union or intersection members'); |
||||
$this->compile('native-class-union-signature.php'); |
||||
} |
||||
|
||||
public function testRejectsIncorrectNativeKeywordReturnType(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('must return exactly `array`'); |
||||
$this->compile('native-class-keyword-return-type.php'); |
||||
} |
||||
|
||||
public function testRejectsMissingNativeKeywordMethod(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('must define `toInt()`'); |
||||
$this->compile('native-class-keyword-missing.php'); |
||||
} |
||||
|
||||
public function testRejectsNativeObjectPassedToJsonEncode(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native objects cannot cross a dynamic PHP/ZendVM call boundary'); |
||||
$this->compile('native-class-json-encode.php'); |
||||
} |
||||
|
||||
public function testRejectsNativeObjectFromUntypedReturn(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native object return values require an explicit native class return type'); |
||||
$this->compile('native-class-untyped-return.php'); |
||||
} |
||||
|
||||
public function testRejectsNativeObjectFromMixedReturn(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native object return values require an explicit native class return type'); |
||||
$this->compile('native-class-mixed-return.php'); |
||||
} |
||||
|
||||
public function testRejectsNativeObjectPassedToInterfaceParameter(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native objects cannot be converted to interface `NativeInterfaceArgumentContract`'); |
||||
$this->compile('native-class-interface-argument.php'); |
||||
} |
||||
|
||||
public function testRejectsNativeObjectAssignedToInterfaceVariable(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native objects cannot be assigned to interface-typed variables'); |
||||
$this->compile('native-class-interface-assignment.php'); |
||||
} |
||||
|
||||
public function testRejectsNativeObjectAssignedToInterfaceProperty(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native objects cannot be assigned to interface-typed properties'); |
||||
$this->compile('native-class-interface-property.php'); |
||||
} |
||||
|
||||
public function testRejectsNativeObjectReturnedAsInterface(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native objects cannot be returned as interface `NativeInterfaceReturnContract`'); |
||||
$this->compile('native-class-interface-return.php'); |
||||
} |
||||
|
||||
public function testRejectsDynamicMagicMethod(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native classes do not support dynamic magic method `__get()`'); |
||||
$this->compile('native-class-dynamic-magic-method.php'); |
||||
} |
||||
|
||||
public function testRejectsDynamicStaticMagicMethodBeforeGenericStaticDiagnostic(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native classes do not support dynamic magic method `__callStatic()`'); |
||||
$this->compile('native-class-dynamic-static-magic-method.php'); |
||||
} |
||||
|
||||
public function testRejectsDynamicMagicMethodInjectedByTrait(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native classes do not support dynamic magic method `__serialize()`'); |
||||
$this->compile('native-class-trait-dynamic-magic-method.php'); |
||||
} |
||||
|
||||
public function testDynamicMagicMethodDenyListIsComplete(): void |
||||
{ |
||||
$trait = new \ReflectionClass(\TypePhp\NativeClass\NativeClassSupportTrait::class); |
||||
$constant = $trait->getReflectionConstant('UNSUPPORTED_NATIVE_MAGIC_METHODS'); |
||||
$this->assertNotFalse($constant); |
||||
$this->assertSame([ |
||||
'__call', |
||||
'__callstatic', |
||||
'__get', |
||||
'__set', |
||||
'__isset', |
||||
'__unset', |
||||
'__sleep', |
||||
'__wakeup', |
||||
'__serialize', |
||||
'__unserialize', |
||||
'__set_state', |
||||
'__debuginfo', |
||||
], array_keys($constant->getValue())); |
||||
} |
||||
|
||||
public function testRejectsStaticMethodInjectedByTrait(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('Native class static methods are not supported'); |
||||
$this->compile('native-class-trait-static-method.php'); |
||||
} |
||||
|
||||
public function testRejectsMissingInternalInterfaceMethod(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('must implement method `Countable::count()`'); |
||||
$this->compile('native-class-internal-interface-missing-method.php'); |
||||
} |
||||
|
||||
public function testRejectsNonPublicInternalInterfaceMethod(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('must be compatible with `Countable::count()`'); |
||||
$this->compile('native-class-internal-interface-visibility.php'); |
||||
} |
||||
|
||||
public function testRejectsNarrowedInternalInterfaceParameter(): void |
||||
{ |
||||
$this->expectException(TestError::class); |
||||
$this->expectExceptionMessage('must be compatible with `ArrayAccess::offsetExists()`'); |
||||
$this->compile('native-class-internal-interface-parameter.php'); |
||||
} |
||||
} |
||||
@ -0,0 +1,866 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP. |
||||
* |
||||
* @link https://www.swoole.com/ |
||||
* @contact service@swoole.com |
||||
*/ |
||||
|
||||
namespace TypePhp\NativeClass; |
||||
|
||||
use TypePhp\Entity\ArgInfo; |
||||
use TypePhp\Entity\ClassDef; |
||||
use TypePhp\Entity\FunctionDef; |
||||
use TypePhp\Entity\MethodDef; |
||||
use TypePhp\Entity\PropertyDef; |
||||
use TypePhp\Resolver\Reflection; |
||||
use PhpParser\Modifiers; |
||||
use TypePhp\Type; |
||||
use PhpParser\NodeAbstract; |
||||
use PhpParser\Node; |
||||
|
||||
trait NativeClassSupportTrait |
||||
{ |
||||
/** |
||||
* Magic methods whose semantics require Zend object handlers, runtime |
||||
* method resolution, dynamic properties, or Zend serialization state. |
||||
* Native classes deliberately have none of those runtime facilities. |
||||
*/ |
||||
private const UNSUPPORTED_NATIVE_MAGIC_METHODS = [ |
||||
'__call' => true, |
||||
'__callstatic' => true, |
||||
'__get' => true, |
||||
'__set' => true, |
||||
'__isset' => true, |
||||
'__unset' => true, |
||||
'__sleep' => true, |
||||
'__wakeup' => true, |
||||
'__serialize' => true, |
||||
'__unserialize' => true, |
||||
'__set_state' => true, |
||||
'__debuginfo' => true, |
||||
]; |
||||
|
||||
protected function assertNativeMagicMethodSupported(NodeAbstract $node, string $method): void |
||||
{ |
||||
if (!$this->classDef?->nativeObject |
||||
|| !isset(self::UNSUPPORTED_NATIVE_MAGIC_METHODS[strtolower($method)]) |
||||
) { |
||||
return; |
||||
} |
||||
$this->fatalError( |
||||
$node, |
||||
"Native classes do not support dynamic magic method `{$method}()`", |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Native classes have no zend_class_entry, so Zend cannot perform the |
||||
* normal MINIT-time interface verification for them. Convert an internal |
||||
* reflection signature into the compiler's existing MethodDef model and |
||||
* run the same compatibility checker used for project interfaces. |
||||
*/ |
||||
protected function checkInternalInterfaceImplementation( |
||||
NodeAbstract $node, |
||||
ClassDef $classDef, |
||||
string $interfaceName, |
||||
): void { |
||||
$interface = Reflection::getClass($interfaceName); |
||||
if ($interface === null) { |
||||
$this->fatalError($node, "Internal interface `{$interfaceName}` is not available"); |
||||
} |
||||
|
||||
foreach ($interface->getMethods() as $method) { |
||||
$childMethodDef = $this->findClassMethodDef($classDef, $method->getName(), $classDef->isAbstract()); |
||||
if ($childMethodDef === null) { |
||||
if ($classDef->isAbstract()) { |
||||
continue; |
||||
} |
||||
$this->fatalError( |
||||
$node, |
||||
"Class `{$classDef->getNamespacedName(false)}` must implement method " . |
||||
"`{$interfaceName}::{$method->getName()}()`", |
||||
); |
||||
} |
||||
$this->validateMethodOverrideSignature( |
||||
$childMethodDef->node ?? $node, |
||||
$method->getName(), |
||||
$childMethodDef, |
||||
$this->createInternalInterfaceMethodDef($method), |
||||
$interfaceName, |
||||
); |
||||
} |
||||
} |
||||
|
||||
private function createInternalInterfaceMethodDef(\ReflectionMethod $method): MethodDef |
||||
{ |
||||
$flags = Modifiers::PUBLIC | Modifiers::ABSTRACT; |
||||
if ($method->isStatic()) { |
||||
$flags |= Modifiers::STATIC; |
||||
} |
||||
|
||||
$methodDef = new MethodDef($flags, $method->getName()); |
||||
$functionDef = new FunctionDef($method->getName(), Type::VAR, ''); |
||||
$functionDef->method = true; |
||||
$functionDef->declaringClass = $method->getDeclaringClass()->getName(); |
||||
$functionDef->returnsByRef = $method->returnsReference(); |
||||
$functionDef->returnTypeUndeclared = $method->getReturnType() === null; |
||||
$this->applyReflectedReturnType($functionDef, $method->getReturnType(), $method->getDeclaringClass()); |
||||
|
||||
foreach ($method->getParameters() as $parameter) { |
||||
$argument = new ArgInfo(); |
||||
$argument->name = $parameter->getName(); |
||||
$argument->phpName = $parameter->getName(); |
||||
$argument->byRef = $parameter->isPassedByReference(); |
||||
$argument->variadic = $parameter->isVariadic(); |
||||
$argument->undeclared = $parameter->getType() === null; |
||||
$argument->nullable = $parameter->allowsNull(); |
||||
$this->applyReflectedParameterType($argument, $parameter->getType(), $method->getDeclaringClass()); |
||||
if ($parameter->isOptional() || $parameter->isVariadic()) { |
||||
// Compatibility only needs to distinguish required from |
||||
// optional parameters; the concrete default is irrelevant. |
||||
$argument->defaultValue = new Node\Expr\ConstFetch(new Node\Name('null')); |
||||
} |
||||
$functionDef->argInfoList[] = $argument; |
||||
} |
||||
$functionDef->argCountRequired = $method->getNumberOfRequiredParameters(); |
||||
$methodDef->functionDef = $functionDef; |
||||
return $methodDef; |
||||
} |
||||
|
||||
private function applyReflectedReturnType( |
||||
FunctionDef $function, |
||||
?\ReflectionType $type, |
||||
\ReflectionClass $declaringClass, |
||||
): void { |
||||
if ($type === null) { |
||||
$function->returnTypeStr = ''; |
||||
return; |
||||
} |
||||
$node = $this->reflectionTypeToNode($type, $declaringClass); |
||||
$function->returnTypeStr = $this->typeCheckNodeToString($node); |
||||
if ($node instanceof Node\NullableType |
||||
|| $node instanceof Node\UnionType |
||||
|| $node instanceof Node\IntersectionType |
||||
) { |
||||
$typeInfo = $this->buildTypeCheckFromNode($node); |
||||
$function->returnType = Type::VAR; |
||||
$function->returnTypeCheck = $typeInfo['check']; |
||||
$function->returnTypeNode = $node; |
||||
return; |
||||
} |
||||
[$function->returnType, $function->returnClass] = $this->resolveReflectedNamedType($node); |
||||
} |
||||
|
||||
private function applyReflectedParameterType( |
||||
ArgInfo $argument, |
||||
?\ReflectionType $type, |
||||
\ReflectionClass $declaringClass, |
||||
): void { |
||||
if ($type === null) { |
||||
$argument->type = Type::VAR; |
||||
return; |
||||
} |
||||
$node = $this->reflectionTypeToNode($type, $declaringClass); |
||||
$argument->typeStr = $this->typeCheckNodeToString($node); |
||||
if ($node instanceof Node\NullableType |
||||
|| $node instanceof Node\UnionType |
||||
|| $node instanceof Node\IntersectionType |
||||
) { |
||||
$typeInfo = $this->buildTypeCheckFromNode($node); |
||||
$argument->type = Type::VAR; |
||||
$argument->typeCheck = $typeInfo['check']; |
||||
$argument->typeNode = $node; |
||||
return; |
||||
} |
||||
[$argument->type, $argument->declaredClass] = $this->resolveReflectedNamedType($node); |
||||
if ($argument->declaredClass !== '' && !$this->isInterface($argument->declaredClass)) { |
||||
$argument->class = $argument->declaredClass; |
||||
} |
||||
$argument->explicitMixed = strtolower($argument->typeStr) === 'mixed'; |
||||
} |
||||
|
||||
/** @return array{string, string} */ |
||||
private function resolveReflectedNamedType(NodeAbstract $node): array |
||||
{ |
||||
$name = $this->parseIdentifier($node); |
||||
$lower = strtolower(ltrim($name, '\\')); |
||||
if (isset($this->zendTypeMap[$lower])) { |
||||
return [$this->getTypeFromZendType($lower), '']; |
||||
} |
||||
return [Type::OBJECT, ltrim($name, '\\')]; |
||||
} |
||||
|
||||
private function reflectionTypeToNode( |
||||
\ReflectionType $type, |
||||
\ReflectionClass $declaringClass, |
||||
bool $allowNullableWrapper = true, |
||||
): NodeAbstract { |
||||
if ($type instanceof \ReflectionUnionType) { |
||||
return new Node\UnionType(array_map( |
||||
fn (\ReflectionType $member): NodeAbstract => |
||||
$this->reflectionTypeToNode($member, $declaringClass, false), |
||||
$type->getTypes(), |
||||
)); |
||||
} |
||||
if ($type instanceof \ReflectionIntersectionType) { |
||||
return new Node\IntersectionType(array_map( |
||||
fn (\ReflectionType $member): NodeAbstract => |
||||
$this->reflectionTypeToNode($member, $declaringClass, false), |
||||
$type->getTypes(), |
||||
)); |
||||
} |
||||
|
||||
/** @var \ReflectionNamedType $type */ |
||||
$name = $type->getName(); |
||||
$lower = strtolower($name); |
||||
if ($lower === 'self') { |
||||
$node = new Node\Name\FullyQualified($declaringClass->getName()); |
||||
} elseif ($lower === 'parent') { |
||||
$parent = $declaringClass->getParentClass(); |
||||
$node = $parent === false |
||||
? new Node\Name('parent') |
||||
: new Node\Name\FullyQualified($parent->getName()); |
||||
} elseif ($lower === 'static') { |
||||
$node = new Node\Name('static'); |
||||
} elseif ($type->isBuiltin()) { |
||||
$node = new Node\Identifier($name); |
||||
} else { |
||||
$node = new Node\Name\FullyQualified($name); |
||||
} |
||||
|
||||
if ($allowNullableWrapper |
||||
&& $type->allowsNull() |
||||
&& !in_array($lower, ['mixed', 'null'], true) |
||||
) { |
||||
return new Node\NullableType($node); |
||||
} |
||||
return $node; |
||||
} |
||||
|
||||
protected function isNativeObjectClass(string $class): bool |
||||
{ |
||||
$class = ltrim($class, '\\'); |
||||
return $class !== '' && $this->hasClass($class) && $this->getClass($class)->nativeObject; |
||||
} |
||||
|
||||
protected function getNativeObjectCppName(string|ClassDef $class): string |
||||
{ |
||||
$classDef = $class instanceof ClassDef ? $class : $this->getClass(ltrim($class, '\\')); |
||||
return self::PREFIX . $this->getNativeName('', $classDef->namespace, $classDef->name); |
||||
} |
||||
|
||||
protected function getNativeObjectDescriptorName(string|ClassDef $class): string |
||||
{ |
||||
return $this->getNativeObjectCppName($class) . '__type'; |
||||
} |
||||
|
||||
protected function getNativeObjectPointerType(string|ClassDef $class): string |
||||
{ |
||||
return $this->getNativeObjectCppName($class) . ' *'; |
||||
} |
||||
|
||||
protected function getNativeObjectArgumentType(ArgInfo $argument): ?string |
||||
{ |
||||
$class = $argument->declaredClass ?: $argument->class; |
||||
if ((!$argument->byRef && $argument->type !== Type::OBJECT) |
||||
|| !$this->isNativeObjectClass($class) |
||||
) { |
||||
return null; |
||||
} |
||||
return $this->getNativeObjectPointerType($class) . ($argument->byRef ? '&' : ''); |
||||
} |
||||
|
||||
protected function resolveNullableNativeObjectType(?NodeAbstract $type, int $declarationKind): ?array |
||||
{ |
||||
$inner = null; |
||||
if ($type instanceof Node\NullableType) { |
||||
$inner = $type->type; |
||||
} elseif ($type instanceof Node\UnionType && count($type->types) === 2) { |
||||
foreach ($type->types as $member) { |
||||
if ($member instanceof Node\Identifier && strtolower($member->toString()) === 'null') { |
||||
continue; |
||||
} |
||||
if ($inner !== null) { |
||||
return null; |
||||
} |
||||
$inner = $member; |
||||
} |
||||
} |
||||
if (!$inner instanceof Node\Name) { |
||||
return null; |
||||
} |
||||
[$innerType, $class] = $this->resolveTypeDecl($inner, $declarationKind); |
||||
if ($innerType !== Type::OBJECT || !$this->isNativeObjectClass($class)) { |
||||
return null; |
||||
} |
||||
return [Type::OBJECT, $class]; |
||||
} |
||||
|
||||
/** @return list<string> */ |
||||
protected function getNativeObjectClassesFromTypeNode(?NodeAbstract $type, int $declarationKind): array |
||||
{ |
||||
if ($type === null || $type instanceof Node\Identifier) { |
||||
return []; |
||||
} |
||||
if ($type instanceof Node\NullableType) { |
||||
return $this->getNativeObjectClassesFromTypeNode($type->type, $declarationKind); |
||||
} |
||||
if ($type instanceof Node\UnionType || $type instanceof Node\IntersectionType) { |
||||
$classes = []; |
||||
foreach ($type->types as $member) { |
||||
foreach ($this->getNativeObjectClassesFromTypeNode($member, $declarationKind) as $class) { |
||||
$classes[strtolower($class)] = $class; |
||||
} |
||||
} |
||||
return array_values($classes); |
||||
} |
||||
if (!$type instanceof Node\Name) { |
||||
return []; |
||||
} |
||||
[$resolvedType, $class] = $this->resolveTypeDecl($type, $declarationKind); |
||||
return $resolvedType === Type::OBJECT && $this->isNativeObjectClass($class) ? [$class] : []; |
||||
} |
||||
|
||||
/** |
||||
* Resolve a common Native pointer type for value-selection branches. |
||||
* Null is accepted as the empty state; any Zend/non-object branch makes |
||||
* the expression unsuitable for the Native object model. |
||||
* |
||||
* @param list<NodeAbstract> $expressions |
||||
*/ |
||||
protected function getCommonNativeObjectExpressionClass(array $expressions): string |
||||
{ |
||||
$common = ''; |
||||
foreach ($expressions as $expression) { |
||||
if ($this->isNull($expression)) { |
||||
continue; |
||||
} |
||||
$class = $this->detectClassOfExpr($expression); |
||||
if (!$this->isNativeObjectClass($class)) { |
||||
return ''; |
||||
} |
||||
if ($common === '') { |
||||
$common = $class; |
||||
continue; |
||||
} |
||||
if ($this->isObjectClassStaticallyAssignableTo($class, $common)) { |
||||
continue; |
||||
} |
||||
if ($this->isObjectClassStaticallyAssignableTo($common, $class)) { |
||||
$common = $class; |
||||
continue; |
||||
} |
||||
return ''; |
||||
} |
||||
return $common; |
||||
} |
||||
|
||||
protected function assertSupportedNativeObjectTypeNode( |
||||
?NodeAbstract $type, |
||||
int $declarationKind, |
||||
NodeAbstract $errorNode, |
||||
): void { |
||||
if (!$type instanceof Node\UnionType && !$type instanceof Node\IntersectionType) { |
||||
return; |
||||
} |
||||
$nativeClasses = $this->getNativeObjectClassesFromTypeNode($type, $declarationKind); |
||||
if ($nativeClasses !== [] && $this->resolveNullableNativeObjectType($type, $declarationKind) === null) { |
||||
$this->fatalError( |
||||
$errorNode, |
||||
'Native object types cannot be combined with other union or intersection members', |
||||
); |
||||
} |
||||
} |
||||
|
||||
protected function getNativeObjectReturnType(FunctionDef $function): ?string |
||||
{ |
||||
if ($function->returnType !== Type::OBJECT || !$this->isNativeObjectClass($function->returnClass)) { |
||||
return null; |
||||
} |
||||
return $this->getNativeObjectPointerType($function->returnClass); |
||||
} |
||||
|
||||
protected function getNativeObjectMethodThisType(FunctionDef $function): ?string |
||||
{ |
||||
if (!$function->method || !$this->isNativeObjectClass($function->declaringClass)) { |
||||
return null; |
||||
} |
||||
return $this->getNativeObjectCppName($function->declaringClass) . ' &'; |
||||
} |
||||
|
||||
protected function functionUsesNativeObject(FunctionDef $function): bool |
||||
{ |
||||
if ($this->getNativeObjectReturnType($function) !== null |
||||
|| $this->getNativeObjectMethodThisType($function) !== null |
||||
) { |
||||
return true; |
||||
} |
||||
foreach ($function->argInfoList as $argument) { |
||||
if ($this->getNativeObjectArgumentType($argument) !== null) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
protected function genNativeObjectParameterChecks(FunctionDef $function): string |
||||
{ |
||||
$code = ''; |
||||
foreach ($function->argInfoList as $argument) { |
||||
$class = $argument->declaredClass ?: $argument->class; |
||||
if (!$argument->nullable && $this->isNativeObjectClass($class)) { |
||||
$code .= $this->getIndent() . 'php::nativeGcRequireObject(' |
||||
. $argument->name . ', "' . addslashes($class) . '");' . PHP_EOL; |
||||
} |
||||
} |
||||
return $code; |
||||
} |
||||
|
||||
protected function addNativeObject(string $name, string $class): void |
||||
{ |
||||
$this->context->nativeObjects[$name] = ltrim($class, '\\'); |
||||
$this->context->objects[$name] = ltrim($class, '\\'); |
||||
} |
||||
|
||||
protected function isNativeObjectVar(string $name): bool |
||||
{ |
||||
return isset($this->context->nativeObjects[$name]); |
||||
} |
||||
|
||||
protected function getNativeObjectVarClass(string $name): string |
||||
{ |
||||
return $this->context->nativeObjects[$name] ?? ''; |
||||
} |
||||
|
||||
protected function getNativeObjectReceiver(string $name): string |
||||
{ |
||||
if ($name === 'this_') { |
||||
return 'this_'; |
||||
} |
||||
$class = $this->getNativeObjectVarClass($name); |
||||
return 'php::nativeDeref(' . $name . ', "' . addslashes($class) . '")'; |
||||
} |
||||
|
||||
protected function getNativeObjectMemberReceiver(string $name): string |
||||
{ |
||||
return $this->getNativeObjectReceiver($name) . '.'; |
||||
} |
||||
|
||||
/** |
||||
* Materialize a Native-producing expression as a precisely rooted local. |
||||
* This is shared by chained method and property access so neither path can |
||||
* accidentally pass a Native pointer through php::Variant. |
||||
*/ |
||||
protected function materializeNativeObjectReceiver(NodeAbstract $expr, string $class): string |
||||
{ |
||||
[$receiver, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr); |
||||
$this->appendCapturedStmtLinesToContext($beforeStmts); |
||||
$object = $this->genTmpVarName(); |
||||
$this->addLocalVar($object, $this->getNativeObjectPointerType($class)); |
||||
$this->addNativeObject($object, $class); |
||||
$this->context->beforeStmtLines[] = $object . ' = ' . $receiver . ';'; |
||||
$this->appendCapturedStmtLinesToContext($afterStmts); |
||||
// Keep the temporary in the precise root frame while the complete PHP |
||||
// statement executes, then release that root at statement end. |
||||
$this->context->afterStmtLines[] = $object . ' = nullptr;'; |
||||
return $object; |
||||
} |
||||
|
||||
protected function findNativeObjectProperty(string $class, string $property): ?PropertyDef |
||||
{ |
||||
while ($class !== '' && $this->hasClass($class)) { |
||||
$classDef = $this->getClass($class); |
||||
if ($classDef->hasProperty($property)) { |
||||
return $classDef->getProperty($property); |
||||
} |
||||
$class = $classDef->extends; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
protected function findNativeObjectMethod(string $class, string $method): ?MethodDef |
||||
{ |
||||
while ($class !== '' && $this->isNativeObjectClass($class)) { |
||||
$classDef = $this->getClass($class); |
||||
if ($classDef->hasMethod($method)) { |
||||
return $classDef->getMethod($method); |
||||
} |
||||
$class = $classDef->extends; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Native objects cannot fall back to PHPX/Zend conversion helpers. A |
||||
* keyword conversion is therefore a statically checked ordinary Native |
||||
* method call. __toString() is accepted as the PHP-compatible spelling of |
||||
* toString(). |
||||
*/ |
||||
protected function resolveNativeObjectKeywordMethod( |
||||
NodeAbstract $node, |
||||
string $class, |
||||
string $method, |
||||
): string { |
||||
$expectedType = self::KEYWORD_METHOD_MAP[$method] ?? null; |
||||
if ($expectedType === null) { |
||||
return $method; |
||||
} |
||||
$resolvedMethod = $method; |
||||
$methodDef = $this->findNativeObjectMethod($class, $resolvedMethod); |
||||
if ($method === 'toString' && $methodDef === null) { |
||||
$resolvedMethod = '__toString'; |
||||
$methodDef = $this->findNativeObjectMethod($class, $resolvedMethod); |
||||
} |
||||
if ($methodDef === null) { |
||||
$this->fatalError($node, "Native class `{$class}` must define `{$method}()` for this conversion"); |
||||
} |
||||
$function = $methodDef->functionDef; |
||||
if ($function->argInfoList !== []) { |
||||
$this->fatalError($node, "Native conversion method `{$class}::{$resolvedMethod}()` must not accept arguments"); |
||||
} |
||||
if ($function->returnsByRef || $function->returnNullable || $function->returnType !== $expectedType) { |
||||
$expectedTypeName = match ($expectedType) { |
||||
Type::INT => 'int', |
||||
Type::FLOAT => 'float', |
||||
Type::STR => 'string', |
||||
Type::BOOL => 'bool', |
||||
Type::ARRAY => 'array', |
||||
Type::STREAM => 'Stream', |
||||
Type::BIGINT => 'BigInt', |
||||
Type::BIGFLOAT => 'BigFloat', |
||||
Type::DECIMAL => 'Decimal', |
||||
Type::OBJECT => 'object', |
||||
Type::VAR => 'mixed', |
||||
default => $expectedType, |
||||
}; |
||||
$this->fatalError( |
||||
$node, |
||||
"Native conversion method `{$class}::{$resolvedMethod}()` must return exactly `{$expectedTypeName}`", |
||||
); |
||||
} |
||||
return $resolvedMethod; |
||||
} |
||||
|
||||
protected function getNativeVirtualMethodName(string $method): string |
||||
{ |
||||
return '__typephp_virtual_' . strtolower($method); |
||||
} |
||||
|
||||
protected function isNativeVirtualMethod(ClassDef $class, MethodDef $method): bool |
||||
{ |
||||
if ($method->flags & (Modifiers::STATIC | Modifiers::PRIVATE | Modifiers::FINAL | Modifiers::ABSTRACT)) { |
||||
return false; |
||||
} |
||||
if (in_array(strtolower($method->name), ['__construct', '__destruct', '__clone'], true)) { |
||||
return false; |
||||
} |
||||
if ($this->isOverrideMethod($class->getNamespacedName(false) . '::' . $method->name)) { |
||||
return true; |
||||
} |
||||
$parent = $class->extends; |
||||
while ($parent !== '' && $this->hasClass($parent)) { |
||||
$parentDef = $this->getClass($parent); |
||||
if ($parentDef->hasMethod($method->name)) { |
||||
return true; |
||||
} |
||||
$parent = $parentDef->extends; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
protected function getNativeMethodReturnCppType(FunctionDef $function): string |
||||
{ |
||||
return $function->returnsByRef |
||||
? Type::REF |
||||
: ($this->getNativeObjectReturnType($function) ?? $function->returnType); |
||||
} |
||||
|
||||
protected function getNativeMethodParameterDeclarations(FunctionDef $function): string |
||||
{ |
||||
$args = []; |
||||
foreach ($function->argInfoList as $argument) { |
||||
if ($argument->variadic) { |
||||
$args[] = Type::ARRAY . ' ' . $argument->name; |
||||
} else { |
||||
$args[] = $this->genArgumentDeclaration($argument); |
||||
} |
||||
} |
||||
return implode(', ', $args); |
||||
} |
||||
|
||||
protected function getNativeObjectPropertyType(PropertyDef $property): string |
||||
{ |
||||
if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) { |
||||
return $this->getNativeObjectPointerType($property->class); |
||||
} |
||||
return match ($property->type) { |
||||
Type::STREAM, Type::BOX => Type::VAR, |
||||
default => $property->type, |
||||
}; |
||||
} |
||||
|
||||
protected function getNativeObjectPropertyCppName( |
||||
string|PropertyDef $property, |
||||
string|ClassDef|null $declaringClass = null, |
||||
): string |
||||
{ |
||||
$name = $property instanceof PropertyDef ? $property->name : $property; |
||||
if (!$property instanceof PropertyDef || !$property->isPrivate()) { |
||||
return $this->escapeVarName($name); |
||||
} |
||||
if ($declaringClass === null) { |
||||
throw new \LogicException('Native private property field requires its declaring class'); |
||||
} |
||||
return '__private_' . $this->getNativeObjectCppName($declaringClass) |
||||
. '__' . $this->escapeVarName($name); |
||||
} |
||||
|
||||
protected function isNativeObjectForbiddenPropertyType(PropertyDef $property): bool |
||||
{ |
||||
if (in_array($property->type, [ |
||||
Type::BOX, |
||||
Type::STD_ARRAY, |
||||
Type::STD_VECTOR, |
||||
Type::STD_MAP, |
||||
Type::STD_ORDERED_MAP, |
||||
], true)) { |
||||
return true; |
||||
} |
||||
return in_array(strtolower(ltrim($property->class, '\\')), [ |
||||
'std\\array', |
||||
'std\\vector', |
||||
'std\\map', |
||||
'std\\ordered_map', |
||||
], true); |
||||
} |
||||
|
||||
protected function isNativeObjectInheritedPropertyRedeclaration( |
||||
ClassDef $class, |
||||
PropertyDef $property, |
||||
): bool { |
||||
$parent = $class->extends; |
||||
while ($parent !== '' && $this->isNativeObjectClass($parent)) { |
||||
$parentDef = $this->getClass($parent); |
||||
if ($parentDef->hasProperty($property->name)) { |
||||
$parentProperty = $parentDef->getProperty($property->name); |
||||
if ($property->isPrivate() || $parentProperty->isPrivate()) { |
||||
return false; |
||||
} |
||||
// Property compatibility is validated separately. TypePHP |
||||
// treats a compatible public/protected redeclaration as the |
||||
// same inherited slot, so a Native child must not emit a |
||||
// second C++ field with the same PHP property name. |
||||
return true; |
||||
} |
||||
$parent = $parentDef->extends; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* C++ requires a base struct to be complete before defining a derived |
||||
* struct. PHP source order has no such restriction, so emit Native class |
||||
* definitions in inheritance order while retaining source order between |
||||
* unrelated classes. |
||||
* |
||||
* @return list<ClassDef> |
||||
*/ |
||||
protected function getNativeObjectClassesInDeclarationOrder(): array |
||||
{ |
||||
$classes = array_values(array_filter( |
||||
$this->symbols->classes(), |
||||
static fn (ClassDef $class): bool => $class->nativeObject, |
||||
)); |
||||
$byName = []; |
||||
foreach ($classes as $class) { |
||||
$byName[strtolower(ltrim($class->getNamespacedName(false), '\\'))] = $class; |
||||
} |
||||
|
||||
$ordered = []; |
||||
$visited = []; |
||||
$visit = function (ClassDef $class) use (&$visit, &$ordered, &$visited, $byName): void { |
||||
$key = strtolower(ltrim($class->getNamespacedName(false), '\\')); |
||||
if (isset($visited[$key])) { |
||||
return; |
||||
} |
||||
$visited[$key] = true; |
||||
$parent = strtolower(ltrim($class->extends, '\\')); |
||||
if ($parent !== '' && isset($byName[$parent])) { |
||||
$visit($byName[$parent]); |
||||
} |
||||
$ordered[] = $class; |
||||
}; |
||||
foreach ($classes as $class) { |
||||
$visit($class); |
||||
} |
||||
return $ordered; |
||||
} |
||||
|
||||
protected function genNativeObjectDeclarations(): string |
||||
{ |
||||
$classes = $this->getNativeObjectClassesInDeclarationOrder(); |
||||
if ($classes === []) { |
||||
return ''; |
||||
} |
||||
|
||||
$code = '// TypePHP Native Object declarations' . PHP_EOL; |
||||
foreach ($classes as $class) { |
||||
$code .= 'struct ' . $this->getNativeObjectCppName($class) . ';' . PHP_EOL; |
||||
} |
||||
$code .= PHP_EOL; |
||||
|
||||
foreach ($classes as $class) { |
||||
$name = $this->getNativeObjectCppName($class); |
||||
$parent = $class->extends !== '' && $this->isNativeObjectClass($class->extends) |
||||
? ' : public ' . $this->getNativeObjectCppName($class->extends) |
||||
: ''; |
||||
$code .= 'struct ' . $name . $parent . ' {' . PHP_EOL; |
||||
foreach ($class->properties as $property) { |
||||
if ($property->flags & Modifiers::STATIC |
||||
|| $this->isNativeObjectInheritedPropertyRedeclaration($class, $property) |
||||
) { |
||||
continue; |
||||
} |
||||
$type = $this->getNativeObjectPropertyType($property); |
||||
// PHPX value types own their storage and must be initialized by |
||||
// their C++ default constructor. PHP-level defaults are applied |
||||
// by the generated allocation/constructor path, not in this |
||||
// shared declaration header (which cannot reference file-local |
||||
// literal tables). |
||||
$default = null; |
||||
if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) { |
||||
$default = 'nullptr'; |
||||
} elseif (in_array($property->type, [Type::INT, Type::FLOAT, Type::BOOL], true)) { |
||||
$default = '0'; |
||||
} |
||||
$code .= ' ' . $type . ' ' . $this->getNativeObjectPropertyCppName($property, $class); |
||||
if ($default !== null) { |
||||
$code .= ' = ' . $default; |
||||
} |
||||
$code .= ';' . PHP_EOL; |
||||
} |
||||
foreach ($class->methods as $method) { |
||||
if (!$this->isNativeVirtualMethod($class, $method)) { |
||||
continue; |
||||
} |
||||
$parentHasMethod = $class->extends !== '' |
||||
&& $this->hasClass($class->extends) |
||||
&& $this->getClass($class->extends)->hasMethod($method->name); |
||||
$code .= ' virtual ' . $this->getNativeMethodReturnCppType($method->functionDef) |
||||
. ' ' . $this->getNativeVirtualMethodName($method->name) . '(' |
||||
. $this->getNativeMethodParameterDeclarations($method->functionDef) . ')' |
||||
. ($parentHasMethod ? ' override' : '') . ';' . PHP_EOL; |
||||
} |
||||
$code .= '};' . PHP_EOL; |
||||
$code .= 'void ' . $name . '__initialize(' . $name . ' &object);' . PHP_EOL; |
||||
$code .= 'void ' . $name . '__gc_trace(void *object, php::NativeMarker &marker);' . PHP_EOL; |
||||
$code .= 'extern const php::NativeTypeDescriptor ' |
||||
. $this->getNativeObjectDescriptorName($class) . ';' . PHP_EOL . PHP_EOL; |
||||
} |
||||
return $code; |
||||
} |
||||
|
||||
protected function genNativeObjectRuntimeDefinition(ClassDef $class): string |
||||
{ |
||||
$cpp = $this->getNativeObjectCppName($class); |
||||
$prefix = $cpp . '__gc'; |
||||
$code = ''; |
||||
$code .= 'void ' . $cpp . '__initialize(' . $cpp . ' &this_) {' . PHP_EOL; |
||||
if ($class->extends !== '' && $this->isNativeObjectClass($class->extends)) { |
||||
$code .= ' ' . $this->getNativeObjectCppName($class->extends) . '__initialize(this_);' . PHP_EOL; |
||||
} |
||||
foreach ($class->properties as $property) { |
||||
if ($property->isStatic() || $property->default === null) { |
||||
continue; |
||||
} |
||||
if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) { |
||||
$value = 'nullptr'; |
||||
} else { |
||||
$value = $property->default; |
||||
} |
||||
$code .= ' this_.' . $this->getNativeObjectPropertyCppName($property, $class) . ' = ' . $value . ';' . PHP_EOL; |
||||
} |
||||
$code .= '}' . PHP_EOL . PHP_EOL; |
||||
foreach ($class->methods as $method) { |
||||
if (!$this->isNativeVirtualMethod($class, $method)) { |
||||
continue; |
||||
} |
||||
$function = $method->functionDef; |
||||
$returnType = $this->getNativeMethodReturnCppType($function); |
||||
$args = array_map(static fn (ArgInfo $arg): string => $arg->name, $function->argInfoList); |
||||
$nativeFunction = self::PREFIX . $this->getNativeName( |
||||
$method->name, |
||||
$class->namespace, |
||||
$class->name, |
||||
); |
||||
$code .= $returnType . ' ' . $cpp . '::' . $this->getNativeVirtualMethodName($method->name) |
||||
. '(' . $this->getNativeMethodParameterDeclarations($function) . ') {' . PHP_EOL; |
||||
$call = $nativeFunction . '(*this' . ($args === [] ? '' : ', ' . implode(', ', $args)) . ')'; |
||||
$code .= ' ' . ($returnType === Type::VOID ? '' : 'return ') . $call . ';' . PHP_EOL; |
||||
$code .= '}' . PHP_EOL . PHP_EOL; |
||||
} |
||||
$code .= 'void ' . $prefix . '_trace(void *object, php::NativeMarker &marker) {' . PHP_EOL; |
||||
$hasParentTrace = $class->extends !== '' && $this->isNativeObjectClass($class->extends); |
||||
$nativeProperties = array_filter( |
||||
$class->properties, |
||||
fn (PropertyDef $property): bool => !$property->isStatic() |
||||
&& !$this->isNativeObjectInheritedPropertyRedeclaration($class, $property) |
||||
&& $property->type === Type::OBJECT |
||||
&& $this->isNativeObjectClass($property->class), |
||||
); |
||||
if ($hasParentTrace || $nativeProperties !== []) { |
||||
$code .= ' auto &this_ = *static_cast<' . $cpp . ' *>(object);' . PHP_EOL; |
||||
} else { |
||||
$code .= ' (void) object;' . PHP_EOL; |
||||
$code .= ' (void) marker;' . PHP_EOL; |
||||
} |
||||
if ($hasParentTrace) { |
||||
$code .= ' ' . $this->getNativeObjectCppName($class->extends) |
||||
. '__gc_trace(static_cast<' . $this->getNativeObjectCppName($class->extends) . ' *>(&this_), marker);' |
||||
. PHP_EOL; |
||||
} |
||||
foreach ($nativeProperties as $property) { |
||||
$code .= ' marker.mark(this_.' . $this->getNativeObjectPropertyCppName($property, $class) . ');' . PHP_EOL; |
||||
} |
||||
$code .= '}' . PHP_EOL; |
||||
|
||||
$destructors = []; |
||||
$destructorClass = $class; |
||||
while (true) { |
||||
if ($destructorClass->hasMethod('__destruct')) { |
||||
$destructors[] = [ |
||||
self::PREFIX . $this->getNativeName( |
||||
'__destruct', |
||||
$destructorClass->namespace, |
||||
$destructorClass->name, |
||||
), |
||||
$this->getNativeObjectCppName($destructorClass), |
||||
]; |
||||
} |
||||
if ($destructorClass->extends === '' || !$this->isNativeObjectClass($destructorClass->extends)) { |
||||
break; |
||||
} |
||||
$destructorClass = $this->getClass($destructorClass->extends); |
||||
} |
||||
if ($destructors !== []) { |
||||
$code .= 'static void ' . $prefix . '_finalize(void *object) {' . PHP_EOL; |
||||
foreach ($destructors as [$destructor, $destructorCpp]) { |
||||
$code .= ' ' . $destructor . '(*static_cast<' . $destructorCpp . ' *>(object));' . PHP_EOL; |
||||
} |
||||
$code .= '}' . PHP_EOL; |
||||
} |
||||
$code .= 'static void ' . $prefix . '_destroy(void *object) noexcept {' . PHP_EOL; |
||||
$code .= ' static_cast<' . $cpp . ' *>(object)->~' . $cpp . '();' . PHP_EOL; |
||||
$code .= '}' . PHP_EOL; |
||||
$code .= 'const php::NativeTypeDescriptor ' . $this->getNativeObjectDescriptorName($class) . ' = {' . PHP_EOL; |
||||
$code .= ' "' . addslashes($class->getNamespacedName(false)) . '",' . PHP_EOL; |
||||
$code .= ' sizeof(' . $cpp . '),' . PHP_EOL; |
||||
$code .= ' alignof(' . $cpp . '),' . PHP_EOL; |
||||
$code .= ' ' . $prefix . '_trace,' . PHP_EOL; |
||||
$code .= ' ' . ($destructors !== [] ? $prefix . '_finalize' : 'nullptr') . ',' . PHP_EOL; |
||||
$code .= ' ' . $prefix . '_destroy,' . PHP_EOL; |
||||
$code .= '};' . PHP_EOL . PHP_EOL; |
||||
return $code; |
||||
} |
||||
} |
||||
@ -0,0 +1,39 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP. |
||||
* |
||||
* @link https://www.swoole.com/ |
||||
* @contact service@swoole.com |
||||
*/ |
||||
|
||||
namespace TypePhp\Transform; |
||||
|
||||
use PhpParser\Node; |
||||
use PhpParser\NodeVisitorAbstract; |
||||
|
||||
final class NativeClassAttributeLowering extends NodeVisitorAbstract |
||||
{ |
||||
public const string ATTRIBUTE = 'typephpNativeClass'; |
||||
|
||||
public function enterNode(Node $node): null |
||||
{ |
||||
self::lower($node); |
||||
return null; |
||||
} |
||||
|
||||
public static function lower(Node $node): void |
||||
{ |
||||
if (!$node instanceof Node\Stmt\Class_) { |
||||
return; |
||||
} |
||||
if (CompileTimeAttribute::consume($node, 'Native')) { |
||||
$node->setAttribute(self::ATTRIBUTE, true); |
||||
} |
||||
} |
||||
|
||||
public static function isNative(Node $node): bool |
||||
{ |
||||
return $node instanceof Node\Stmt\Class_ |
||||
&& $node->getAttribute(self::ATTRIBUTE, false) === true; |
||||
} |
||||
} |
||||
@ -0,0 +1,41 @@ |
||||
--TEST-- |
||||
Native class: construction, typed properties and direct method calls |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class Point |
||||
{ |
||||
public int $x = 0; |
||||
public int $y = 0; |
||||
|
||||
public function __construct(int $x, int $y) |
||||
{ |
||||
$this->x = $x; |
||||
$this->y = $y; |
||||
} |
||||
|
||||
public function sum(): int |
||||
{ |
||||
return $this->x + $this->y; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$point = new Point(20, 22); |
||||
var_dump($point->x, $point->y, $point->sum()); |
||||
unset($point); |
||||
try { |
||||
$point->sum(); |
||||
} catch (Error $error) { |
||||
echo "null guarded\n"; |
||||
} |
||||
} |
||||
|
||||
?> |
||||
--EXPECT-- |
||||
int(20) |
||||
int(22) |
||||
int(42) |
||||
null guarded |
||||
@ -0,0 +1,30 @@ |
||||
--TEST-- |
||||
Native class: by-reference parameters replace the caller pointer |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeReferenceValue |
||||
{ |
||||
public int $value; |
||||
|
||||
public function __construct(int $value) |
||||
{ |
||||
$this->value = $value; |
||||
} |
||||
} |
||||
|
||||
function replaceNativeReference(NativeReferenceValue &$value): void |
||||
{ |
||||
$value = new NativeReferenceValue(42); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$value = new NativeReferenceValue(1); |
||||
replaceNativeReference($value); |
||||
var_dump($value->value); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(42) |
||||
@ -0,0 +1,36 @@ |
||||
--TEST-- |
||||
Native class: call result arguments are evaluated left-to-right and precisely rooted |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeArgument |
||||
{ |
||||
public string $name; |
||||
|
||||
public function __construct(string $name) |
||||
{ |
||||
$this->name = $name; |
||||
} |
||||
} |
||||
|
||||
function makeNativeArgument(string $name): NativeArgument |
||||
{ |
||||
echo 'make:', $name, PHP_EOL; |
||||
return new NativeArgument($name); |
||||
} |
||||
|
||||
function consumeNativeArguments(NativeArgument $first, NativeArgument $second): void |
||||
{ |
||||
echo $first->name, ':', $second->name, PHP_EOL; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
consumeNativeArguments(makeNativeArgument('A'), makeNativeArgument('B')); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
make:A |
||||
make:B |
||||
A:B |
||||
@ -0,0 +1,22 @@ |
||||
--TEST-- |
||||
Native class: chained assignment remains in the native object model |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeChainedValue |
||||
{ |
||||
public int $value = 42; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$first = $second = new NativeChainedValue(); |
||||
var_dump($first->value, $second->value); |
||||
var_dump($first === $second); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(42) |
||||
int(42) |
||||
bool(true) |
||||
@ -0,0 +1,32 @@ |
||||
--TEST-- |
||||
Native class: method calls accept native object expressions as receivers |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeChainValue |
||||
{ |
||||
public int $value = 42; |
||||
|
||||
public function getValue(): int |
||||
{ |
||||
return $this->value; |
||||
} |
||||
} |
||||
|
||||
function makeNativeChainValue(): NativeChainValue |
||||
{ |
||||
return new NativeChainValue(); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
echo (new NativeChainValue())->getValue(), PHP_EOL; |
||||
echo makeNativeChainValue()->getValue(), PHP_EOL; |
||||
echo makeNativeChainValue()->value, PHP_EOL; |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
42 |
||||
42 |
||||
42 |
||||
@ -0,0 +1,29 @@ |
||||
--TEST-- |
||||
Native class: clone is native and the class remains invisible to ZendVM |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeCloneValue |
||||
{ |
||||
public int $value = 1; |
||||
|
||||
public function __clone(): void |
||||
{ |
||||
$this->value++; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$first = new NativeCloneValue(); |
||||
$second = clone $first; |
||||
var_dump($first->value, $second->value); |
||||
var_dump(class_exists('NativeCloneValue', false)); |
||||
} |
||||
|
||||
?> |
||||
--EXPECT-- |
||||
int(1) |
||||
int(2) |
||||
bool(false) |
||||
@ -0,0 +1,71 @@ |
||||
--TEST-- |
||||
Native class: nullable, union and intersection fields use Var with runtime type checks |
||||
--FILE-- |
||||
<?php |
||||
|
||||
interface NativeCompositeLeft {} |
||||
interface NativeCompositeRight {} |
||||
|
||||
class NativeCompositeBoth implements NativeCompositeLeft, NativeCompositeRight {} |
||||
class NativeCompositeLeftOnly implements NativeCompositeLeft {} |
||||
|
||||
#[Native] |
||||
class NativeCompositeProperties |
||||
{ |
||||
public ?int $nullableInt; |
||||
public int|string|null $unionValue; |
||||
public NativeCompositeLeft&NativeCompositeRight $intersectionValue; |
||||
} |
||||
|
||||
function writeNullable(NativeCompositeProperties $object, mixed $value): void |
||||
{ |
||||
$object->nullableInt = $value; |
||||
} |
||||
|
||||
function writeUnion(NativeCompositeProperties $object, mixed $value): void |
||||
{ |
||||
$object->unionValue = $value; |
||||
} |
||||
|
||||
function writeIntersection(NativeCompositeProperties $object, mixed $value): void |
||||
{ |
||||
$object->intersectionValue = $value; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$object = new NativeCompositeProperties(); |
||||
var_dump($object->nullableInt, $object->unionValue, $object->intersectionValue); |
||||
|
||||
writeNullable($object, 42); |
||||
writeUnion($object, 'ok'); |
||||
writeIntersection($object, new NativeCompositeBoth()); |
||||
var_dump($object->nullableInt, $object->unionValue, $object->intersectionValue::class); |
||||
|
||||
try { |
||||
writeNullable($object, 'bad'); |
||||
} catch (TypeError $error) { |
||||
echo "type error\n"; |
||||
} |
||||
try { |
||||
writeUnion($object, []); |
||||
} catch (TypeError $error) { |
||||
echo "type error\n"; |
||||
} |
||||
try { |
||||
writeIntersection($object, new NativeCompositeLeftOnly()); |
||||
} catch (TypeError $error) { |
||||
echo "type error\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
NULL |
||||
NULL |
||||
NULL |
||||
int(42) |
||||
string(2) "ok" |
||||
string(19) "NativeCompositeBoth" |
||||
type error |
||||
type error |
||||
type error |
||||
@ -0,0 +1,25 @@ |
||||
--TEST-- |
||||
Native class: declarations are emitted in inheritance order |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeOrderChild extends NativeOrderParent |
||||
{ |
||||
public int $child = 2; |
||||
} |
||||
|
||||
#[Native] |
||||
class NativeOrderParent |
||||
{ |
||||
public int $parent = 1; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$value = new NativeOrderChild(); |
||||
echo $value->parent, ':', $value->child, PHP_EOL; |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
1:2 |
||||
@ -0,0 +1,34 @@ |
||||
--TEST-- |
||||
Native class: GC finalization runs destructors from derived to base exactly once |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeDestructorBase |
||||
{ |
||||
public function __destruct() |
||||
{ |
||||
echo 'B'; |
||||
} |
||||
} |
||||
|
||||
#[Native] |
||||
class NativeDestructorChild extends NativeDestructorBase |
||||
{ |
||||
public function __destruct() |
||||
{ |
||||
echo 'C'; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$value = new NativeDestructorChild(); |
||||
$value = null; |
||||
echo "done\n"; |
||||
} |
||||
|
||||
?> |
||||
--EXPECT-- |
||||
done |
||||
CB |
||||
@ -0,0 +1,37 @@ |
||||
--TEST-- |
||||
Native class: tracing GC collects cycles and runs destructors once |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeNode |
||||
{ |
||||
public ?NativeNode $next = null; |
||||
public string $name = ''; |
||||
|
||||
public function __construct(string $name) |
||||
{ |
||||
$this->name = $name; |
||||
} |
||||
|
||||
public function __destruct() |
||||
{ |
||||
echo $this->name; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$a = new NativeNode('A'); |
||||
$b = new NativeNode('B'); |
||||
$a->next = $b; |
||||
$b->next = $a; |
||||
$a = null; |
||||
$b = null; |
||||
echo "done\n"; |
||||
} |
||||
|
||||
?> |
||||
--EXPECT-- |
||||
done |
||||
BA |
||||
@ -0,0 +1,33 @@ |
||||
--TEST-- |
||||
Native class: Getter and Setter generators lower to direct native methods |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeGeneratedAccessors |
||||
{ |
||||
#[Getter] |
||||
#[Setter] |
||||
private int $value = 1; |
||||
|
||||
public function __construct( |
||||
#[Getter] |
||||
private string $name = 'native', |
||||
) { |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$object = new NativeGeneratedAccessors(); |
||||
var_dump($object->getValue()); |
||||
var_dump($object->getName()); |
||||
$object->setValue(42); |
||||
var_dump($object->getValue()); |
||||
} |
||||
|
||||
?> |
||||
--EXPECT-- |
||||
int(1) |
||||
string(6) "native" |
||||
int(42) |
||||
@ -0,0 +1,42 @@ |
||||
--TEST-- |
||||
Native class: TypePHP globals and static locals retain request-rooted native objects |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeCounter |
||||
{ |
||||
public int $value; |
||||
} |
||||
|
||||
function initializeGlobal(): void |
||||
{ |
||||
global $nativeGlobal; |
||||
$nativeGlobal = new NativeCounter(); |
||||
$nativeGlobal->value = 40; |
||||
} |
||||
|
||||
function readGlobal(): int |
||||
{ |
||||
global $nativeGlobal; |
||||
return $nativeGlobal->value; |
||||
} |
||||
|
||||
function nextStatic(): int |
||||
{ |
||||
static $counter = new NativeCounter(); |
||||
return ++$counter->value; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
initializeGlobal(); |
||||
var_dump(readGlobal()); |
||||
var_dump(nextStatic()); |
||||
var_dump(nextStatic()); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(40) |
||||
int(1) |
||||
int(2) |
||||
@ -0,0 +1,38 @@ |
||||
--TEST-- |
||||
Native class: compatible inherited property declarations reuse one C++ field |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativePropertyBase |
||||
{ |
||||
public int $value = 1; |
||||
|
||||
public function writeFromBase(int $value): void |
||||
{ |
||||
$this->value = $value; |
||||
} |
||||
} |
||||
|
||||
#[Native] |
||||
class NativePropertyChild extends NativePropertyBase |
||||
{ |
||||
public int $value = 2; |
||||
|
||||
public function readFromChild(): int |
||||
{ |
||||
return $this->value; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$value = new NativePropertyChild(); |
||||
var_dump($value->readFromChild()); |
||||
$value->writeFromBase(42); |
||||
var_dump($value->readFromChild()); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(2) |
||||
int(42) |
||||
@ -0,0 +1,41 @@ |
||||
--TEST-- |
||||
Native class: statically resolved instanceof is folded at compile time |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeInstanceofBase |
||||
{ |
||||
} |
||||
|
||||
#[Native] |
||||
class NativeInstanceofChild extends NativeInstanceofBase |
||||
{ |
||||
} |
||||
|
||||
#[Native] |
||||
class NativeInstanceofOther |
||||
{ |
||||
} |
||||
|
||||
function makeInstanceofChild(): NativeInstanceofChild |
||||
{ |
||||
echo "made\n"; |
||||
return new NativeInstanceofChild(); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$object = new NativeInstanceofChild(); |
||||
var_dump($object instanceof NativeInstanceofChild); |
||||
var_dump($object instanceof NativeInstanceofBase); |
||||
var_dump($object instanceof NativeInstanceofOther); |
||||
var_dump(makeInstanceofChild() instanceof NativeInstanceofChild); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
bool(true) |
||||
bool(true) |
||||
bool(false) |
||||
made |
||||
bool(true) |
||||
@ -0,0 +1,24 @@ |
||||
--TEST-- |
||||
Native class: internal interfaces are compile-time contracts |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeCountableValue implements Countable |
||||
{ |
||||
public function count(): int |
||||
{ |
||||
return 3; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$value = new NativeCountableValue(); |
||||
var_dump($value->count()); |
||||
var_dump($value instanceof Countable); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(3) |
||||
bool(true) |
||||
@ -0,0 +1,74 @@ |
||||
--TEST-- |
||||
Native class: keyword conversions lower to exactly typed native methods |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeConversions |
||||
{ |
||||
public int $value = 7; |
||||
|
||||
public function toArray(): array |
||||
{ |
||||
return [$this->value]; |
||||
} |
||||
|
||||
public function toInt(): int |
||||
{ |
||||
return $this->value; |
||||
} |
||||
|
||||
public function toFloat(): float |
||||
{ |
||||
return $this->value + 0.5; |
||||
} |
||||
|
||||
public function toBool(): bool |
||||
{ |
||||
return $this->value !== 0; |
||||
} |
||||
|
||||
public function toString(): string |
||||
{ |
||||
return 'value=' . $this->value; |
||||
} |
||||
} |
||||
|
||||
#[Native] |
||||
class NativeMagicString |
||||
{ |
||||
public function __toString(): string |
||||
{ |
||||
return 'magic'; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$value = new NativeConversions(); |
||||
var_dump($value->toArray()); |
||||
var_dump($value->toInt()); |
||||
var_dump($value->toFloat()); |
||||
var_dump($value->toBool()); |
||||
var_dump($value->toString()); |
||||
var_dump((string) $value); |
||||
var_dump(strval($value)); |
||||
|
||||
$magic = new NativeMagicString(); |
||||
var_dump($magic->toString()); |
||||
var_dump((string) $magic); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(1) { |
||||
[0]=> |
||||
int(7) |
||||
} |
||||
int(7) |
||||
float(7.5) |
||||
bool(true) |
||||
string(7) "value=7" |
||||
string(7) "value=7" |
||||
string(7) "value=7" |
||||
string(5) "magic" |
||||
string(5) "magic" |
||||
@ -0,0 +1,35 @@ |
||||
--TEST-- |
||||
Native class: __toString and __invoke lower to direct native calls |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeCallableLabel |
||||
{ |
||||
public string $label = 'native'; |
||||
|
||||
public function __toString(): string |
||||
{ |
||||
return $this->label; |
||||
} |
||||
|
||||
public function __invoke(int $number): string |
||||
{ |
||||
return $this->label . ':' . $number; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$value = new NativeCallableLabel(); |
||||
echo $value, "\n"; |
||||
var_dump((string) $value); |
||||
var_dump('value=' . $value); |
||||
var_dump($value(42)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
native |
||||
string(6) "native" |
||||
string(12) "value=native" |
||||
string(9) "native:42" |
||||
@ -0,0 +1,28 @@ |
||||
--TEST-- |
||||
Native class: non-null parameters are validated at function entry |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeRequiredArgument |
||||
{ |
||||
} |
||||
|
||||
function acceptRequiredNative(NativeRequiredArgument $value): void |
||||
{ |
||||
echo "entered\n"; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$value = new NativeRequiredArgument(); |
||||
unset($value); |
||||
try { |
||||
acceptRequiredNative($value); |
||||
} catch (Error $error) { |
||||
echo "rejected\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
rejected |
||||
@ -0,0 +1,50 @@ |
||||
--TEST-- |
||||
Native class: nullable parameters and returns stay native pointers |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeNullableValue |
||||
{ |
||||
public int $value = 42; |
||||
} |
||||
|
||||
function maybeNative(bool $create): ?NativeNullableValue |
||||
{ |
||||
if ($create) { |
||||
return new NativeNullableValue(); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
function readMaybeNative(?NativeNullableValue $value): int |
||||
{ |
||||
if ($value === null) { |
||||
return -1; |
||||
} |
||||
return $value->value; |
||||
} |
||||
|
||||
function maybeNativeUnion(bool $create): NativeNullableValue|null |
||||
{ |
||||
return $create ? new NativeNullableValue() : null; |
||||
} |
||||
|
||||
function readMaybeNativeUnion(NativeNullableValue|null $value): int |
||||
{ |
||||
return $value === null ? -2 : $value->value; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(readMaybeNative(maybeNative(false))); |
||||
var_dump(readMaybeNative(maybeNative(true))); |
||||
var_dump(readMaybeNativeUnion(maybeNativeUnion(false))); |
||||
var_dump(readMaybeNativeUnion(maybeNativeUnion(true))); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(-1) |
||||
int(42) |
||||
int(-2) |
||||
int(42) |
||||
@ -0,0 +1,43 @@ |
||||
--TEST-- |
||||
Native class: PHPX value properties retain normal string and array behavior |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativePhpValues |
||||
{ |
||||
public string $class = 'reserved'; |
||||
public string $name = 'native'; |
||||
public array $values = [1, 2]; |
||||
public mixed $anything = null; |
||||
public ?int $maybe = null; |
||||
|
||||
public function append(int $value): void |
||||
{ |
||||
$this->values[] = $value; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$object = new NativePhpValues(); |
||||
$object->append(3); |
||||
$object->anything = 42; |
||||
$object->maybe = 7; |
||||
var_dump($object->class, $object->name, $object->values, $object->anything, $object->maybe); |
||||
} |
||||
|
||||
?> |
||||
--EXPECT-- |
||||
string(8) "reserved" |
||||
string(6) "native" |
||||
array(3) { |
||||
[0]=> |
||||
int(1) |
||||
[1]=> |
||||
int(2) |
||||
[2]=> |
||||
int(3) |
||||
} |
||||
int(42) |
||||
int(7) |
||||
@ -0,0 +1,51 @@ |
||||
--TEST-- |
||||
Native class: parent and child private properties use independent native slots |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativePrivateBase |
||||
{ |
||||
private int $value = 10; |
||||
|
||||
public function baseValue(): int |
||||
{ |
||||
return $this->value; |
||||
} |
||||
|
||||
public function setBaseValue(int $value): void |
||||
{ |
||||
$this->value = $value; |
||||
} |
||||
} |
||||
|
||||
#[Native] |
||||
class NativePrivateChild extends NativePrivateBase |
||||
{ |
||||
private int $value = 20; |
||||
|
||||
public function childValue(): int |
||||
{ |
||||
return $this->value; |
||||
} |
||||
|
||||
public function setChildValue(int $value): void |
||||
{ |
||||
$this->value = $value; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$value = new NativePrivateChild(); |
||||
var_dump($value->baseValue(), $value->childValue()); |
||||
$value->setBaseValue(11); |
||||
$value->setChildValue(22); |
||||
var_dump($value->baseValue(), $value->childValue()); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(10) |
||||
int(20) |
||||
int(11) |
||||
int(22) |
||||
@ -0,0 +1,32 @@ |
||||
--TEST-- |
||||
Native class: PHP 8.4 property hooks use native getter and setter calls |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeHookValue |
||||
{ |
||||
private int $stored = 1; |
||||
|
||||
public int $value { |
||||
get { |
||||
return $this->stored * 2; |
||||
} |
||||
set(int $value) { |
||||
$this->stored = $value; |
||||
} |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$object = new NativeHookValue(); |
||||
var_dump($object->value); |
||||
$object->value = 21; |
||||
var_dump($object->value); |
||||
} |
||||
|
||||
?> |
||||
--EXPECT-- |
||||
int(2) |
||||
int(42) |
||||
@ -0,0 +1,73 @@ |
||||
--TEST-- |
||||
Native class: trait composition, inheritance and interface contracts |
||||
--FILE-- |
||||
<?php |
||||
|
||||
interface Named |
||||
{ |
||||
public function label(): string; |
||||
} |
||||
|
||||
trait HasCounter |
||||
{ |
||||
public int $count = 0; |
||||
|
||||
public function increment(): void |
||||
{ |
||||
$this->count++; |
||||
} |
||||
} |
||||
|
||||
#[Native] |
||||
class NativeBase |
||||
{ |
||||
use HasCounter; |
||||
|
||||
public function __construct(int $initial) |
||||
{ |
||||
$this->count = $initial; |
||||
} |
||||
|
||||
public function label(): string |
||||
{ |
||||
return 'base'; |
||||
} |
||||
} |
||||
|
||||
#[Native] |
||||
class NativeChild extends NativeBase implements Named |
||||
{ |
||||
public function __construct() |
||||
{ |
||||
parent::__construct(2); |
||||
} |
||||
|
||||
public function label(): string |
||||
{ |
||||
return 'child'; |
||||
} |
||||
} |
||||
|
||||
function nativeLabel(NativeBase $value): string |
||||
{ |
||||
return $value->label(); |
||||
} |
||||
|
||||
function makeNativeChild(): NativeChild |
||||
{ |
||||
return new NativeChild(); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$value = makeNativeChild(); |
||||
$value->increment(); |
||||
var_dump($value->label(), nativeLabel($value), $value->count, $value instanceof Named); |
||||
} |
||||
|
||||
?> |
||||
--EXPECT-- |
||||
string(5) "child" |
||||
string(5) "child" |
||||
int(3) |
||||
bool(true) |
||||
@ -0,0 +1,28 @@ |
||||
--TEST-- |
||||
Native class: unset and null clear only the local pointer slot |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeAliasValue |
||||
{ |
||||
public int $value = 42; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$first = new NativeAliasValue(); |
||||
$alias = $first; |
||||
unset($first); |
||||
var_dump($first === null, $alias->value); |
||||
|
||||
$second = $alias; |
||||
$alias = null; |
||||
var_dump($alias === null, $second->value); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
bool(true) |
||||
int(42) |
||||
bool(true) |
||||
int(42) |
||||
@ -0,0 +1,44 @@ |
||||
--TEST-- |
||||
Native class: ternary, match and coalesce preserve native pointer types |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeSelectedValue |
||||
{ |
||||
public int $value; |
||||
|
||||
public function __construct(int $value) |
||||
{ |
||||
$this->value = $value; |
||||
} |
||||
} |
||||
|
||||
function selectWithMatch(int $kind): NativeSelectedValue |
||||
{ |
||||
return match ($kind) { |
||||
1 => new NativeSelectedValue(10), |
||||
default => new NativeSelectedValue(20), |
||||
}; |
||||
} |
||||
|
||||
function selectWithCoalesce(?NativeSelectedValue $value): NativeSelectedValue |
||||
{ |
||||
return $value ?? new NativeSelectedValue(30); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$first = true ? new NativeSelectedValue(1) : new NativeSelectedValue(2); |
||||
var_dump($first->value); |
||||
var_dump(selectWithMatch(1)->value, selectWithMatch(2)->value); |
||||
var_dump(selectWithCoalesce(null)->value); |
||||
var_dump(selectWithCoalesce($first)->value); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(1) |
||||
int(10) |
||||
int(20) |
||||
int(30) |
||||
int(1) |
||||
@ -0,0 +1,30 @@ |
||||
--TEST-- |
||||
Native class: native-only methods on Zend objects use direct calls and stay out of Zend metadata |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeMethodArgument |
||||
{ |
||||
public int $value = 42; |
||||
} |
||||
|
||||
class ZendMethodHost |
||||
{ |
||||
public function read(NativeMethodArgument $value): int |
||||
{ |
||||
return $value->value; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$host = new ZendMethodHost(); |
||||
$value = new NativeMethodArgument(); |
||||
var_dump($host->read($value)); |
||||
var_dump(method_exists($host, 'read')); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(42) |
||||
bool(false) |
||||
@ -0,0 +1,43 @@ |
||||
--TEST-- |
||||
Native class: properties without explicit defaults use their type zero values |
||||
--FILE-- |
||||
<?php |
||||
|
||||
#[Native] |
||||
class NativeZeroValue |
||||
{ |
||||
public bool $enabled; |
||||
public int $count; |
||||
public float $ratio; |
||||
public string $name; |
||||
public array $items; |
||||
public mixed $value; |
||||
public ?stdClass $object; |
||||
public ?NativeZeroValue $child; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$value = new NativeZeroValue(); |
||||
var_dump( |
||||
$value->enabled, |
||||
$value->count, |
||||
$value->ratio, |
||||
$value->name, |
||||
$value->items, |
||||
$value->value, |
||||
$value->object, |
||||
$value->child === null, |
||||
); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
bool(false) |
||||
int(0) |
||||
float(0) |
||||
string(0) "" |
||||
array(0) { |
||||
} |
||||
NULL |
||||
NULL |
||||
bool(true) |
||||
Loading…
Reference in new issue