fix(compiler): preserve signatures for typed property method calls (#104)
* fix(compiler): preserve signatures for typed property method calls * fix(compiler): separate property signatures from dispatchmaster
parent
13889e9a30
commit
a1efda0f7f
10 changed files with 794 additions and 3 deletions
@ -0,0 +1,24 @@ |
|||||||
|
# Typed property method calls |
||||||
|
|
||||||
|
This benchmark calls a final class method through a declared object property |
||||||
|
1,000,000 times, returning checksum `7000000`. It isolates receiver/property and |
||||||
|
method-call overhead; it is not a whole-application performance estimate. |
||||||
|
|
||||||
|
Build from the repository root with a matching PHPX runtime: |
||||||
|
|
||||||
|
```sh |
||||||
|
php bin/tpc.php benchmark/property-method-call/project.yml --no-progress \ |
||||||
|
--build-dir /tmp/typephp-property-method-build -o /tmp/property-method-call |
||||||
|
/tmp/property-method-call |
||||||
|
``` |
||||||
|
|
||||||
|
For a before/after comparison, use the same PHPX, compiler flags and workload in |
||||||
|
both checkouts, separate build directories, and alternate the two binaries after |
||||||
|
a warmup. Verify equal output before comparing elapsed time. |
||||||
|
|
||||||
|
An initial Linux ARM64/PHP 8.5.10 ZTS/PHPX `4b3a472` O2 comparison against TypePHP |
||||||
|
`a1782233` measured five-run median process times of 29.97 ms before and 22.63 ms |
||||||
|
after (24.5% lower elapsed time). Timings include process startup. The normal |
||||||
|
22-command dungeon scenario improved only about 2.4%, and its exception-heavy |
||||||
|
28-command scenario showed no reliable improvement. These results demonstrate a |
||||||
|
local call-path improvement, not a claim that all applications become 24.5% faster. |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
<?php |
||||||
|
final class PropertyCounter |
||||||
|
{ |
||||||
|
public function value(): int { return 7; } |
||||||
|
} |
||||||
|
final class PropertyCounterHolder |
||||||
|
{ |
||||||
|
public PropertyCounter $counter; |
||||||
|
public function __construct() { $this->counter = new PropertyCounter(); } |
||||||
|
public function run(int $count): int |
||||||
|
{ |
||||||
|
$sum = 0; |
||||||
|
for ($i = 0; $i < $count; ++$i) { |
||||||
|
$sum += $this->counter->value(); |
||||||
|
} |
||||||
|
return $sum; |
||||||
|
} |
||||||
|
} |
||||||
|
function main(int $argc, array $argv): void |
||||||
|
{ |
||||||
|
echo (new PropertyCounterHolder())->run(1000000), "\n"; |
||||||
|
} |
||||||
@ -0,0 +1,5 @@ |
|||||||
|
name: property_method_call |
||||||
|
build-mode: bin |
||||||
|
optimize: 2 |
||||||
|
sources: |
||||||
|
- main.php |
||||||
@ -0,0 +1,248 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
final class PropertyCallTarget |
||||||
|
{ |
||||||
|
public function __construct(public int $id = 1) {} |
||||||
|
|
||||||
|
public function record(array &$events, int $value = 0): int |
||||||
|
{ |
||||||
|
$events[] = $this->id + $value; |
||||||
|
return $this->id; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class PropertyCallBase |
||||||
|
{ |
||||||
|
public function value(): int { return 1; } |
||||||
|
} |
||||||
|
|
||||||
|
final class PropertyCallChild extends PropertyCallBase |
||||||
|
{ |
||||||
|
public function value(): int { return 2; } |
||||||
|
} |
||||||
|
|
||||||
|
final class PropertyCallHolder |
||||||
|
{ |
||||||
|
public PropertyCallTarget $target; |
||||||
|
public PropertyCallBase $polymorphic; |
||||||
|
public ?PropertyCallTarget $nullable = null; |
||||||
|
public PropertyCallTarget $uninitialized; |
||||||
|
public int $reads = 0; |
||||||
|
public PropertyCallTarget $hooked { |
||||||
|
get { |
||||||
|
++$this->reads; |
||||||
|
return $this->target; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public function __construct() |
||||||
|
{ |
||||||
|
$this->target = new PropertyCallTarget(); |
||||||
|
$this->polymorphic = new PropertyCallChild(); |
||||||
|
} |
||||||
|
|
||||||
|
public function replace(): int |
||||||
|
{ |
||||||
|
$this->target = new PropertyCallTarget(9); |
||||||
|
return 3; |
||||||
|
} |
||||||
|
|
||||||
|
public function checkHook(array &$events): array |
||||||
|
{ |
||||||
|
$id = $this->hooked->record($events); |
||||||
|
return [$id, $this->reads]; |
||||||
|
} |
||||||
|
|
||||||
|
public function checkUninitialized(array &$events): string |
||||||
|
{ |
||||||
|
try { |
||||||
|
$this->uninitialized->record($events); |
||||||
|
} catch (Error $error) { |
||||||
|
return 'uninitialized'; |
||||||
|
} |
||||||
|
return 'unexpected'; |
||||||
|
} |
||||||
|
|
||||||
|
public function run(array &$events): array |
||||||
|
{ |
||||||
|
$first = $this->target->record($events, $this->replace()); |
||||||
|
$second = $this->target->record(value: 2, events: $events); |
||||||
|
return [$first, $second, $this->polymorphic->value(), $this->nullable?->record($events)]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class PrivateMethodPropertyBase |
||||||
|
{ |
||||||
|
private function value(): string |
||||||
|
{ |
||||||
|
return 'base'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class PrivateMethodPropertyChild extends PrivateMethodPropertyBase |
||||||
|
{ |
||||||
|
public function value(): string |
||||||
|
{ |
||||||
|
return 'child'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class PrivateMethodPropertyHolder |
||||||
|
{ |
||||||
|
public PrivateMethodPropertyBase $target; |
||||||
|
|
||||||
|
public function run(): string |
||||||
|
{ |
||||||
|
return $this->target->value(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
interface InterfacePropertyRecorder |
||||||
|
{ |
||||||
|
public function record(array &$events): void; |
||||||
|
} |
||||||
|
|
||||||
|
final class InterfacePropertyRecorderImpl implements InterfacePropertyRecorder |
||||||
|
{ |
||||||
|
public function record(array &$events): void |
||||||
|
{ |
||||||
|
$events[] = 'recorded'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class InterfacePropertyHolder |
||||||
|
{ |
||||||
|
public InterfacePropertyRecorder $target; |
||||||
|
|
||||||
|
public function run(array &$events): void |
||||||
|
{ |
||||||
|
$this->target->record($events); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class StaticPropertyCallTarget |
||||||
|
{ |
||||||
|
public function record(array &$events): void |
||||||
|
{ |
||||||
|
$events[] = 'recorded'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class StaticPropertyCallHolder |
||||||
|
{ |
||||||
|
public static StaticPropertyCallTarget $target; |
||||||
|
|
||||||
|
public static function run(array &$events): void |
||||||
|
{ |
||||||
|
self::$target->record($events); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function unrelatedRecordSignature(array &$events): void |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
function dynamicReceiverRecordCall(mixed $target, array $events): void |
||||||
|
{ |
||||||
|
$target->unrelatedRecordSignature($events); |
||||||
|
} |
||||||
|
|
||||||
|
final class MissingSignaturePropertyTarget |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
function missingSignaturePropertyMethod(array &$events): void |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
final class MissingSignaturePropertyHolder |
||||||
|
{ |
||||||
|
public MissingSignaturePropertyTarget $target; |
||||||
|
|
||||||
|
public function run(array $events): void |
||||||
|
{ |
||||||
|
$this->target->missingSignaturePropertyMethod($events); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class LexicalPrivatePropertyBase |
||||||
|
{ |
||||||
|
public LexicalPrivateFinalPropertyChild $finalTarget; |
||||||
|
public LexicalPrivateOpenPropertyChild $openTarget; |
||||||
|
|
||||||
|
private function record(array &$events): void |
||||||
|
{ |
||||||
|
$events[] = 'base'; |
||||||
|
} |
||||||
|
|
||||||
|
public function runFinal(array &$events): void |
||||||
|
{ |
||||||
|
$this->finalTarget->record($events); |
||||||
|
} |
||||||
|
|
||||||
|
public function runOpen(array &$events): void |
||||||
|
{ |
||||||
|
$this->openTarget->record($events); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class LexicalPrivateFinalPropertyChild extends LexicalPrivatePropertyBase |
||||||
|
{ |
||||||
|
public function record(array $events): void |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class LexicalPrivateOpenPropertyChild extends LexicalPrivatePropertyBase |
||||||
|
{ |
||||||
|
public function record(array $events): void |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class MagicPrivatePropertyTarget |
||||||
|
{ |
||||||
|
private function hidden(array &$events): void |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public function __call(string $name, array $arguments): void |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class MagicPrivatePropertyHolder |
||||||
|
{ |
||||||
|
public MagicPrivatePropertyTarget $target; |
||||||
|
|
||||||
|
public function run(array $events): void |
||||||
|
{ |
||||||
|
$this->target->hidden($events); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class MagicMissingPropertyTarget |
||||||
|
{ |
||||||
|
public function __call(string $name, array $arguments): void |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class MagicMissingPropertyHolder |
||||||
|
{ |
||||||
|
public MagicMissingPropertyTarget $target; |
||||||
|
|
||||||
|
public function run(array $events): void |
||||||
|
{ |
||||||
|
$this->target->missing($events); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$holder = new PropertyCallHolder(); |
||||||
|
$events = []; |
||||||
|
$result = $holder->run($events); |
||||||
|
$hook = $holder->checkHook($events); |
||||||
|
echo json_encode([$result, $events, $holder->checkUninitialized($events), $hook], JSON_THROW_ON_ERROR), "\n"; |
||||||
|
} |
||||||
@ -0,0 +1,143 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
use TypePhp\CompilerTest; |
||||||
|
|
||||||
|
final class TypedPropertyMethodCallTest extends BaseTest |
||||||
|
{ |
||||||
|
public function testKnownPropertyReceiverUsesDirectCallWithReferenceArguments(): void |
||||||
|
{ |
||||||
|
global $translator; |
||||||
|
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); |
||||||
|
$translator = $compiler; |
||||||
|
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/typed-property-method-call.php'; |
||||||
|
$compiler->addFiles([$source]); |
||||||
|
$compiler->prepareFile($source); |
||||||
|
$generated = $compiler->convertFile($source); |
||||||
|
$code = file_get_contents($generated); |
||||||
|
self::assertIsString($code); |
||||||
|
self::assertSame(1, preg_match( |
||||||
|
'/php::Array php_propertycallholder__run\(.*?\) \{(?<body>.*?)\n\}/s', |
||||||
|
$code, |
||||||
|
$matches, |
||||||
|
)); |
||||||
|
self::assertSame(2, substr_count($matches['body'], 'php_propertycalltarget__record(')); |
||||||
|
self::assertStringNotContainsString('php_propertycallbase__value(', $matches['body']); |
||||||
|
} |
||||||
|
|
||||||
|
public function testNonFinalPropertyReceiverUsesRuntimeDispatch(): void |
||||||
|
{ |
||||||
|
$code = $this->compileFixture(); |
||||||
|
self::assertSame(1, preg_match( |
||||||
|
'/php::\w+ php_privatemethodpropertyholder__run\(.*?\) \{(?<body>.*?)\n\}/s', |
||||||
|
$code, |
||||||
|
$matches, |
||||||
|
)); |
||||||
|
self::assertStringNotContainsString('php_privatemethodpropertybase__value(', $matches['body']); |
||||||
|
self::assertMatchesRegularExpression('/(?:typephp_call_method(?:_scoped)?_cached|\.call)\(/', $matches['body']); |
||||||
|
} |
||||||
|
|
||||||
|
public function testInterfacePropertyReceiverUsesInterfaceReferenceSignature(): void |
||||||
|
{ |
||||||
|
$code = $this->compileFixture(); |
||||||
|
self::assertSame(1, preg_match( |
||||||
|
'/void php_interfacepropertyholder__run\(.*?\) \{(?<body>.*?)\n\}/s', |
||||||
|
$code, |
||||||
|
$matches, |
||||||
|
)); |
||||||
|
self::assertMatchesRegularExpression('/(?:RefWrap|toReference|\.ref\()/i', $matches['body']); |
||||||
|
self::assertMatchesRegularExpression('/(?:typephp_call_method_cached|\.call)\(/', $matches['body']); |
||||||
|
} |
||||||
|
|
||||||
|
public function testStaticPropertyReceiverUsesDirectCallWithReferenceArguments(): void |
||||||
|
{ |
||||||
|
$code = $this->compileFixture(); |
||||||
|
self::assertSame(1, preg_match( |
||||||
|
'/void php_staticpropertycallholder__run\(.*?\) \{(?<body>.*?)\n\}/s', |
||||||
|
$code, |
||||||
|
$matches, |
||||||
|
)); |
||||||
|
self::assertStringContainsString('php_staticpropertycalltarget__record(', $matches['body']); |
||||||
|
} |
||||||
|
|
||||||
|
public function testUnknownReceiverDoesNotUseSameNamedGlobalFunctionSignature(): void |
||||||
|
{ |
||||||
|
$code = $this->compileFixture(); |
||||||
|
self::assertSame(1, preg_match( |
||||||
|
'/void php_dynamicreceiverrecordcall\(.*?\) \{(?<body>.*?)\n\}/s', |
||||||
|
$code, |
||||||
|
$matches, |
||||||
|
)); |
||||||
|
self::assertStringNotContainsString('RefWrap', $matches['body']); |
||||||
|
self::assertStringNotContainsString('.ref()', $matches['body']); |
||||||
|
} |
||||||
|
|
||||||
|
public function testMissingTypedPropertySignatureDoesNotUseSameNamedGlobalFunction(): void |
||||||
|
{ |
||||||
|
$code = $this->compileFixture(); |
||||||
|
self::assertSame(1, preg_match( |
||||||
|
'/void php_missingsignaturepropertyholder__run\(.*?\) \{(?<body>.*?)\n\}/s', |
||||||
|
$code, |
||||||
|
$matches, |
||||||
|
)); |
||||||
|
self::assertStringNotContainsString('RefWrap', $matches['body']); |
||||||
|
self::assertStringNotContainsString('.ref()', $matches['body']); |
||||||
|
} |
||||||
|
|
||||||
|
public function testLexicalPrivateMethodWinsOverPropertyClassMethod(): void |
||||||
|
{ |
||||||
|
$code = $this->compileFixture(); |
||||||
|
foreach (['runfinal', 'runopen'] as $method) { |
||||||
|
self::assertSame(1, preg_match( |
||||||
|
'/void php_lexicalprivatepropertybase__' . $method . '\\(.*?\\) \\{(?<body>.*?)\\n\\}/s', |
||||||
|
$code, |
||||||
|
$matches, |
||||||
|
)); |
||||||
|
self::assertStringNotContainsString('php_lexicalprivatepropertybase__record(', $matches['body']); |
||||||
|
self::assertStringNotContainsString('php_lexicalprivatefinalpropertychild__record(', $matches['body']); |
||||||
|
self::assertStringNotContainsString('php_lexicalprivateopenpropertychild__record(', $matches['body']); |
||||||
|
self::assertStringContainsString('typephp_call_method_scoped_cached(', $matches['body']); |
||||||
|
self::assertMatchesRegularExpression('/(?:RefWrap|toReference|\.ref\()/i', $matches['body']); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public function testFinalPropertyPrivateMethodUsesMagicRuntimeDispatch(): void |
||||||
|
{ |
||||||
|
$code = $this->compileFixture(); |
||||||
|
self::assertSame(1, preg_match( |
||||||
|
'/void php_magicprivatepropertyholder__run\(.*?\) \{(?<body>.*?)\n\}/s', |
||||||
|
$code, |
||||||
|
$matches, |
||||||
|
)); |
||||||
|
self::assertStringNotContainsString('php_magicprivatepropertytarget__hidden(', $matches['body']); |
||||||
|
self::assertMatchesRegularExpression('/(?:typephp_call_method(?:_scoped)?_cached|\.call)\(/', $matches['body']); |
||||||
|
self::assertStringNotContainsString('RefWrap', $matches['body']); |
||||||
|
self::assertStringNotContainsString('.ref()', $matches['body']); |
||||||
|
} |
||||||
|
|
||||||
|
public function testFinalPropertyMissingMethodRetainsDirectMagicOptimization(): void |
||||||
|
{ |
||||||
|
$code = $this->compileFixture(); |
||||||
|
self::assertSame(1, preg_match( |
||||||
|
'/void php_magicmissingpropertyholder__run\(.*?\) \{(?<body>.*?)\n\}/s', |
||||||
|
$code, |
||||||
|
$matches, |
||||||
|
)); |
||||||
|
self::assertStringContainsString('php_magicmissingpropertytarget____call(', $matches['body']); |
||||||
|
self::assertStringNotContainsString('typephp_call_method', $matches['body']); |
||||||
|
self::assertStringNotContainsString('RefWrap', $matches['body']); |
||||||
|
} |
||||||
|
|
||||||
|
private function compileFixture(): string |
||||||
|
{ |
||||||
|
global $translator; |
||||||
|
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); |
||||||
|
$translator = $compiler; |
||||||
|
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/typed-property-method-call.php'; |
||||||
|
$compiler->addFiles([$source]); |
||||||
|
$compiler->prepareFile($source); |
||||||
|
$generated = $compiler->convertFile($source); |
||||||
|
$code = file_get_contents($generated); |
||||||
|
self::assertIsString($code); |
||||||
|
return $code; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
--TEST-- |
||||||
|
Interface typed property receiver preserves by-reference arguments |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
interface TypedPropertyEventRecorder |
||||||
|
{ |
||||||
|
public function record(array &$events): void; |
||||||
|
} |
||||||
|
|
||||||
|
final class TypedPropertyEventRecorderImpl implements TypedPropertyEventRecorder |
||||||
|
{ |
||||||
|
public function record(array &$events): void |
||||||
|
{ |
||||||
|
$events[] = 'recorded'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class TypedPropertyInterfaceHolder |
||||||
|
{ |
||||||
|
public TypedPropertyEventRecorder $target; |
||||||
|
|
||||||
|
public function __construct() |
||||||
|
{ |
||||||
|
$this->target = new TypedPropertyEventRecorderImpl(); |
||||||
|
} |
||||||
|
|
||||||
|
public function run(array &$events): void |
||||||
|
{ |
||||||
|
$this->target->record($events); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$events = []; |
||||||
|
$holder = new TypedPropertyInterfaceHolder(); |
||||||
|
$holder->run($events); |
||||||
|
echo json_encode($events, JSON_THROW_ON_ERROR), "\n"; |
||||||
|
} |
||||||
|
|
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
["recorded"] |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
--TEST-- |
||||||
|
Typed property receiver dispatches a child public method over a parent private method |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class TypedPropertyPrivateBase |
||||||
|
{ |
||||||
|
private function value(): string |
||||||
|
{ |
||||||
|
return 'base'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class TypedPropertyPrivateChild extends TypedPropertyPrivateBase |
||||||
|
{ |
||||||
|
public function value(): string |
||||||
|
{ |
||||||
|
return 'child'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class TypedPropertyPrivateHolder |
||||||
|
{ |
||||||
|
public TypedPropertyPrivateBase $target; |
||||||
|
|
||||||
|
public function __construct() |
||||||
|
{ |
||||||
|
$this->target = new TypedPropertyPrivateChild(); |
||||||
|
} |
||||||
|
|
||||||
|
public function run(): string |
||||||
|
{ |
||||||
|
return $this->target->value(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
echo (new TypedPropertyPrivateHolder())->run(), "\n"; |
||||||
|
} |
||||||
|
|
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
child |
||||||
@ -0,0 +1,88 @@ |
|||||||
|
--TEST-- |
||||||
|
Typed property method calls preserve references, receiver order, overrides, hooks and uninitialized access |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
final class PropertyCallTarget |
||||||
|
{ |
||||||
|
public function __construct(public int $id = 1) {} |
||||||
|
|
||||||
|
public function record(array &$events, int $value = 0): int |
||||||
|
{ |
||||||
|
$events[] = $this->id + $value; |
||||||
|
return $this->id; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class PropertyCallBase |
||||||
|
{ |
||||||
|
public function value(): int { return 1; } |
||||||
|
} |
||||||
|
|
||||||
|
final class PropertyCallChild extends PropertyCallBase |
||||||
|
{ |
||||||
|
public function value(): int { return 2; } |
||||||
|
} |
||||||
|
|
||||||
|
final class PropertyCallHolder |
||||||
|
{ |
||||||
|
public PropertyCallTarget $target; |
||||||
|
public PropertyCallBase $polymorphic; |
||||||
|
public ?PropertyCallTarget $nullable = null; |
||||||
|
public PropertyCallTarget $uninitialized; |
||||||
|
public int $reads = 0; |
||||||
|
public PropertyCallTarget $hooked { |
||||||
|
get { |
||||||
|
++$this->reads; |
||||||
|
return $this->target; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public function __construct() |
||||||
|
{ |
||||||
|
$this->target = new PropertyCallTarget(); |
||||||
|
$this->polymorphic = new PropertyCallChild(); |
||||||
|
} |
||||||
|
|
||||||
|
public function replace(): int |
||||||
|
{ |
||||||
|
$this->target = new PropertyCallTarget(9); |
||||||
|
return 3; |
||||||
|
} |
||||||
|
|
||||||
|
public function checkHook(array &$events): array |
||||||
|
{ |
||||||
|
$id = $this->hooked->record($events); |
||||||
|
return [$id, $this->reads]; |
||||||
|
} |
||||||
|
|
||||||
|
public function checkUninitialized(array &$events): string |
||||||
|
{ |
||||||
|
try { |
||||||
|
$this->uninitialized->record($events); |
||||||
|
} catch (Error $error) { |
||||||
|
return 'uninitialized'; |
||||||
|
} |
||||||
|
return 'unexpected'; |
||||||
|
} |
||||||
|
|
||||||
|
public function run(array &$events): array |
||||||
|
{ |
||||||
|
$first = $this->target->record($events, $this->replace()); |
||||||
|
$second = $this->target->record(value: 2, events: $events); |
||||||
|
return [$first, $second, $this->polymorphic->value(), $this->nullable?->record($events)]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$holder = new PropertyCallHolder(); |
||||||
|
$events = []; |
||||||
|
$result = $holder->run($events); |
||||||
|
$hook = $holder->checkHook($events); |
||||||
|
echo json_encode([$result, $events, $holder->checkUninitialized($events), $hook], JSON_THROW_ON_ERROR), "\n"; |
||||||
|
} |
||||||
|
|
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
[[1,9,2,null],[4,11,9],"uninitialized",[9,1]] |
||||||
@ -0,0 +1,34 @@ |
|||||||
|
--TEST-- |
||||||
|
Non-nullable typed static property receiver preserves by-reference arguments |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
final class TypedStaticPropertyTarget |
||||||
|
{ |
||||||
|
public function record(array &$events): void |
||||||
|
{ |
||||||
|
$events[] = 'recorded'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class TypedStaticPropertyHolder |
||||||
|
{ |
||||||
|
public static TypedStaticPropertyTarget $target; |
||||||
|
|
||||||
|
public static function run(array &$events): void |
||||||
|
{ |
||||||
|
self::$target->record($events); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
TypedStaticPropertyHolder::$target = new TypedStaticPropertyTarget(); |
||||||
|
$events = []; |
||||||
|
TypedStaticPropertyHolder::run($events); |
||||||
|
echo json_encode($events, JSON_THROW_ON_ERROR), "\n"; |
||||||
|
} |
||||||
|
|
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
["recorded"] |
||||||
Loading…
Reference in new issue