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