Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions benchmark/property-method-call/README.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 22 additions & 0 deletions benchmark/property-method-call/main.php
Original file line number Diff line number Diff line change
@@ -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";
}
5 changes: 5 additions & 0 deletions benchmark/property-method-call/project.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: property_method_call
build-mode: bin
optimize: 2
sources:
- main.php
81 changes: 81 additions & 0 deletions phpunit/code/typed-property-method-call.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?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";
}
26 changes: 26 additions & 0 deletions phpunit/src/TypedPropertyMethodCallTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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']);
}
}
30 changes: 29 additions & 1 deletion src/Parser/MethodCallTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ protected function parseMethodCall(Expr\MethodCall $expr): string

$class = '';
$materializedNativeReceiver = false;
$materializedTypedPropertyReceiver = false;
// C++17 sequences a member-call receiver before its arguments, but
// lowering an argument may hoist captured beforeStmtLines ahead of the
// whole call. Materialize an effectful receiver before parsing args.
Expand All @@ -554,6 +555,32 @@ protected function parseMethodCall(Expr\MethodCall $expr): string
$object = $this->materializeNativeObjectReceiver($expr->var, $receiverClass);
$class = $receiverClass;
$materializedNativeReceiver = true;
} elseif ($expr->var instanceof Expr\PropertyFetch && $this->isIdExpr($expr->var->name)) {
// A non-nullable declared object property has a known class, but
// is not a variable receiver. Resolve it before parsing arguments
// and record the materialized Object under that declared class so
// the normal native-method path can retain its argument lowering.
$this->getPropertyIdentifier($expr->var, $expr->var->var, $expr->var->name);
$property = $this->getNativePropertyDef($expr->var);
if ($property !== null
&& $property->type === Type::OBJECT
&& !$property->nullable
&& $property->class !== ''
&& $this->hasClass($property->class)
&& !$this->isNativeObjectClass($property->class)
) {
$object = $this->parseOrderedOperand($expr->var, false, true);
$this->addObject($object, $property->class);
$class = $property->class;
$materializedTypedPropertyReceiver = true;
} else {
$object = empty($expr->args)
? $this->parseIdentifier($expr->var)
: $this->parseOrderedOperand($expr->var, false);
if (empty($expr->args)) {
$object = '(' . $object . ')';
}
}
} else {
$object = empty($expr->args)
? $this->parseIdentifier($expr->var)
Expand Down Expand Up @@ -700,7 +727,8 @@ protected function parseMethodCall(Expr\MethodCall $expr): string
}

// Method calls that can be lowered to a native call
if (($this->isVarExpr($expr->var) || $materializedNativeReceiver) and $this->isNamedMethod($expr->name)) {
if (($this->isVarExpr($expr->var) || $materializedNativeReceiver || $materializedTypedPropertyReceiver)
and $this->isNamedMethod($expr->name)) {
$type = $this->getVarType($object);
if ($class !== '' && $this->isNativeObjectClass($class)) {
// Native objects have their own C++ virtual thunk for an
Expand Down
88 changes: 88 additions & 0 deletions tests/compiler/devirtualize/typed-property-receiver.phpt
Original file line number Diff line number Diff line change
@@ -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]]
Loading