feat(compiler): implement dynamic call caching and static property resolution

- Add function call cache infrastructure with typephp_call_cached wrapper
- Implement method call caching using typephp_call_method_cached for named methods
- Add scoped method call caching via typephp_call_method_scoped_cached for dynamic scopes
- Create late-static-bound class entry tracking with getCalledCeExpr and getCalledClassExpr
- Implement lazy static property slot resolution with typephp_get_static_property_cached
- Add benchmark cases for dynamic static method calls and scoped method invocations
- Update call cache test expectations to reflect new cached call implementations
- Integrate call caching into function and method call compilation paths
- Add nullsafe method call caching support in nullsafe access trait
- Implement static property slot registration and resolution mechanisms
- Update native property handling to use cached static property access
- Add comprehensive test coverage for various dynamic call scenarios
master
韩天峰 2 days ago
parent 02a98ffc61
commit 87c7eda964
  1. 15
      benchmark/dynamic-call/README.md
  2. 152
      benchmark/dynamic-call/benchmark.php
  3. 10
      benchmark/dynamic-call/run.php
  4. 10
      benchmark/static-cache/README.md
  5. 72
      benchmark/static-cache/benchmark.php
  6. 2
      benchmark/static-cache/run.php
  7. 14
      docs/en/SCOPE_MANAGEMENT.md
  8. 14
      docs/zh-cn/SCOPE_MANAGEMENT.md
  9. 31
      phpunit/code/call-cache-sites.php
  10. 27
      phpunit/code/static-property-function-local-cache.php
  11. 12
      phpunit/src/CallCacheCodegenTest.php
  12. 2
      phpunit/src/DevirtualizeOrderTest.php
  13. 6
      phpunit/src/LocalVariableInitializerTest.php
  14. 4
      phpunit/src/LoopControlTest.php
  15. 4
      phpunit/src/MagicCallCodegenTest.php
  16. 12
      phpunit/src/NativePropertyTest.php
  17. 54
      phpunit/src/StaticPropertyFunctionLocalCacheTest.php
  18. 43
      src/CompilerBase.php
  19. 10
      src/Context/FunctionContext.php
  20. 2
      src/Generator/ClosureGenerator.php
  21. 2
      src/Generator/TypeCheckGenerator.php
  22. 6
      src/Parser/ClassConstantFetchTrait.php
  23. 2
      src/Parser/ConstantExpressionTrait.php
  24. 3
      src/Parser/FunctionCallTrait.php
  25. 41
      src/Parser/MethodCallTrait.php
  26. 8
      src/Parser/NullsafeAccessTrait.php
  27. 82
      src/Parser/PropertyAccessTrait.php
  28. 90
      tests/compiler/dynamic_call/call-cache-dispatch.phpt
  29. 89
      tests/compiler/static/static-property-function-local-slot.phpt

@ -10,6 +10,21 @@ It also measures dynamic method names with a stable receiver, alternating
method names, and a fixed method name on changing receiver classes. Those method names, and a fixed method name on changing receiver classes. Those
cases require a class-entry guard in addition to a callable-name guard. cases require a class-entry guard in addition to a callable-name guard.
The `named_method_dynamic_receiver*` cases specifically exercise
`Variant::call(const Variant &, ...)`: the PHP method name is fixed, while the
receiver's concrete class is hidden behind an `object` value. The monomorphic
cases measure the cacheable path with zero and one argument; the polymorphic
case guards against optimizing one runtime class as though it were static.
The `scoped_*` cases exercise private/protected dynamic calls that must resolve
with the compiled method's lexical scope. They are kept separate because a
scoped cache must guard both the target callable and its calling scope.
The `static_*_dynamic` cases exercise direct `$class::method()`,
`Class::$method()`, and `$class::$method()` syntax. These sites still construct
their callable string dynamically, but now reuse the request-local resolution
slot instead of repeating `zend_is_callable_ex()` on every iteration.
The monomorphic string-call cases cover zero, one, two, and four positional The monomorphic string-call cases cover zero, one, two, and four positional
arguments. This separates callable-cache lookup cost from argument arguments. This separates callable-cache lookup cost from argument
materialization cost and protects the small stack-argument fast path. materialization cost and protects the small stack-argument fast path.

@ -82,6 +82,11 @@ final class DynamicCallTarget
return $value + 2; return $value + 2;
} }
public function hitZero(): int
{
return 1;
}
public function __invoke(int $value): int public function __invoke(int $value): int
{ {
return $value + 3; return $value + 3;
@ -96,6 +101,53 @@ final class DynamicCallAlternateTarget
} }
} }
final class ScopedDynamicCallTarget
{
private function hitZero(): int
{
return 1;
}
protected function hitOne(int $value): int
{
return $value + 1;
}
public function runDynamicNameZeroArgs(int $iterations): int
{
$method = 'hitZero';
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$sum += $this->$method();
}
return $sum;
}
public function runDynamicName(int $iterations): int
{
$method = 'hitOne';
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$sum += $this->$method($i);
}
return $sum;
}
public function runNamedDynamicReceiver(object $target, int $iterations): int
{
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$sum += $target->hitOne($i);
}
return $sum;
}
}
function createDynamicMethodReceiver(): object
{
return new DynamicCallTarget();
}
function runDirectCall(int $iterations): int function runDirectCall(int $iterations): int
{ {
$sum = 0; $sum = 0;
@ -207,6 +259,37 @@ function runStaticMethodStringCall(int $iterations): int
return $sum; return $sum;
} }
function runDynamicStaticClassCall(int $iterations): int
{
$class = DynamicCallTarget::class;
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$sum += $class::addOne($i);
}
return $sum;
}
function runDynamicStaticMethodCall(int $iterations): int
{
$method = 'addOne';
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$sum += DynamicCallTarget::$method($i);
}
return $sum;
}
function runDynamicStaticClassAndMethodCall(int $iterations): int
{
$class = DynamicCallTarget::class;
$method = 'addOne';
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$sum += $class::$method($i);
}
return $sum;
}
function runObjectMethodArrayCall(int $iterations): int function runObjectMethodArrayCall(int $iterations): int
{ {
$target = new DynamicCallTarget(); $target = new DynamicCallTarget();
@ -262,6 +345,57 @@ function runPolymorphicMethodReceiverCall(int $iterations): int
return $sum; return $sum;
} }
function runNamedMethodDynamicReceiverZeroArgs(int $iterations): int
{
// The declared `object` return type deliberately hides the concrete class
// from TypePHP while keeping the call site monomorphic at runtime.
$target = createDynamicMethodReceiver();
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$sum += $target->hitZero();
}
return $sum;
}
function runNamedMethodDynamicReceiverCall(int $iterations): int
{
$target = createDynamicMethodReceiver();
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$sum += $target->hitOne($i);
}
return $sum;
}
function runNamedMethodPolymorphicReceiverCall(int $iterations): int
{
$targets = [new DynamicCallTarget(), new DynamicCallAlternateTarget()];
$sum = 0;
for ($i = 0; $i < $iterations; $i++) {
$target = $targets[$i & 1];
$sum += $target->hitOne($i);
}
return $sum;
}
function runScopedMethodNameZeroArgs(int $iterations): int
{
$target = new ScopedDynamicCallTarget();
return $target->runDynamicNameZeroArgs($iterations);
}
function runScopedMethodNameCall(int $iterations): int
{
$target = new ScopedDynamicCallTarget();
return $target->runDynamicName($iterations);
}
function runScopedNamedDynamicReceiverCall(int $iterations): int
{
$target = new ScopedDynamicCallTarget();
return $target->runNamedDynamicReceiver($target, $iterations);
}
function runDynamicCallCase(string $case, int $iterations): int function runDynamicCallCase(string $case, int $iterations): int
{ {
return match ($case) { return match ($case) {
@ -275,11 +409,20 @@ function runDynamicCallCase(string $case, int $iterations): int
'closure_monomorphic' => runMonomorphicClosureCall($iterations), 'closure_monomorphic' => runMonomorphicClosureCall($iterations),
'closure_alternating' => runAlternatingClosureCall($iterations), 'closure_alternating' => runAlternatingClosureCall($iterations),
'static_method_string' => runStaticMethodStringCall($iterations), 'static_method_string' => runStaticMethodStringCall($iterations),
'static_class_dynamic' => runDynamicStaticClassCall($iterations),
'static_method_dynamic' => runDynamicStaticMethodCall($iterations),
'static_class_method_dynamic' => runDynamicStaticClassAndMethodCall($iterations),
'object_method_array' => runObjectMethodArrayCall($iterations), 'object_method_array' => runObjectMethodArrayCall($iterations),
'invokable_object' => runInvokableObjectCall($iterations), 'invokable_object' => runInvokableObjectCall($iterations),
'method_name_monomorphic' => runMonomorphicMethodNameCall($iterations), 'method_name_monomorphic' => runMonomorphicMethodNameCall($iterations),
'method_name_alternating' => runAlternatingMethodNameCall($iterations), 'method_name_alternating' => runAlternatingMethodNameCall($iterations),
'method_receiver_polymorphic' => runPolymorphicMethodReceiverCall($iterations), 'method_receiver_polymorphic' => runPolymorphicMethodReceiverCall($iterations),
'named_method_dynamic_receiver_zero' => runNamedMethodDynamicReceiverZeroArgs($iterations),
'named_method_dynamic_receiver' => runNamedMethodDynamicReceiverCall($iterations),
'named_method_polymorphic_receiver' => runNamedMethodPolymorphicReceiverCall($iterations),
'scoped_method_name_zero' => runScopedMethodNameZeroArgs($iterations),
'scoped_method_name' => runScopedMethodNameCall($iterations),
'scoped_named_dynamic_receiver' => runScopedNamedDynamicReceiverCall($iterations),
default => throw new RuntimeException("Unknown benchmark case: {$case}"), default => throw new RuntimeException("Unknown benchmark case: {$case}"),
}; };
} }
@ -319,11 +462,20 @@ function main(): void
'closure_monomorphic', 'closure_monomorphic',
'closure_alternating', 'closure_alternating',
'static_method_string', 'static_method_string',
'static_class_dynamic',
'static_method_dynamic',
'static_class_method_dynamic',
'object_method_array', 'object_method_array',
'invokable_object', 'invokable_object',
'method_name_monomorphic', 'method_name_monomorphic',
'method_name_alternating', 'method_name_alternating',
'method_receiver_polymorphic', 'method_receiver_polymorphic',
'named_method_dynamic_receiver_zero',
'named_method_dynamic_receiver',
'named_method_polymorphic_receiver',
'scoped_method_name_zero',
'scoped_method_name',
'scoped_named_dynamic_receiver',
] as $case) { ] as $case) {
if (is_string($selectedCase) && $selectedCase !== '' && $selectedCase !== $case) { if (is_string($selectedCase) && $selectedCase !== '' && $selectedCase !== $case) {
continue; continue;

@ -86,6 +86,7 @@ if (!$skipBuild) {
echo "Building TypePHP benchmark (-O3 + LTO)...\n"; echo "Building TypePHP benchmark (-O3 + LTO)...\n";
runDynamicCallCommand([ runDynamicCallCommand([
$compilerPhp, $compilerPhp,
'-n',
$root . '/bin/tpc.php', $root . '/bin/tpc.php',
$project, $project,
'-j', '-j',
@ -135,11 +136,20 @@ $cases = [
'closure_monomorphic', 'closure_monomorphic',
'closure_alternating', 'closure_alternating',
'static_method_string', 'static_method_string',
'static_class_dynamic',
'static_method_dynamic',
'static_class_method_dynamic',
'object_method_array', 'object_method_array',
'invokable_object', 'invokable_object',
'method_name_monomorphic', 'method_name_monomorphic',
'method_name_alternating', 'method_name_alternating',
'method_receiver_polymorphic', 'method_receiver_polymorphic',
'named_method_dynamic_receiver_zero',
'named_method_dynamic_receiver',
'named_method_polymorphic_receiver',
'scoped_method_name_zero',
'scoped_method_name',
'scoped_named_dynamic_receiver',
]; ];
if ($selectedCase !== null && $selectedCase !== '') { if ($selectedCase !== null && $selectedCase !== '') {
$cases = [$selectedCase]; $cases = [$selectedCase];

@ -1,9 +1,11 @@
# Static class cache benchmark # Static class cache benchmark
This benchmark covers a common metadata-cache pattern: a static array keyed by This benchmark contains isolated reads and writes of statically resolved
`static::class`, guarded by `isset()`, plus a wrapper method using `self::$property` and `Class::$property` slots. It also covers a common
`static::method()`. It measures static-property lookup, array lookup, strict metadata-cache pattern: a static array keyed by `static::class`, guarded by
return checks, and late-static dispatch together. `isset()`, plus a wrapper method using `static::method()`. The latter measures
static-property lookup, array lookup, strict return checks, and late-static
dispatch together.
Run it from the repository root against matching Release PHP and PHPX builds: Run it from the repository root against matching Release PHP and PHPX builds:

@ -25,6 +25,68 @@ class StaticCacheData
} }
} }
final class StaticSlotData
{
public static int $counter = 1;
public static function measureSelfRead(): array
{
$best = PHP_FLOAT_MAX;
$checksum = 0;
for ($round = 0; $round < STATIC_CACHE_WARMUPS + STATIC_CACHE_ROUNDS; $round++) {
$sum = 0;
$start = hrtime(true);
for ($i = 0; $i < STATIC_CACHE_ITERATIONS; $i++) {
$sum += self::$counter;
}
$elapsed = hrtime(true) - $start;
if ($round >= STATIC_CACHE_WARMUPS && $elapsed < $best) {
$best = $elapsed;
}
$checksum += $sum;
}
return [$best / STATIC_CACHE_ITERATIONS, $checksum];
}
public static function measureSelfWrite(): array
{
$best = PHP_FLOAT_MAX;
$checksum = 0;
for ($round = 0; $round < STATIC_CACHE_WARMUPS + STATIC_CACHE_ROUNDS; $round++) {
$start = hrtime(true);
for ($i = 0; $i < STATIC_CACHE_ITERATIONS; $i++) {
self::$counter = $i;
}
$elapsed = hrtime(true) - $start;
if ($round >= STATIC_CACHE_WARMUPS && $elapsed < $best) {
$best = $elapsed;
}
$checksum += self::$counter;
}
return [$best / STATIC_CACHE_ITERATIONS, $checksum];
}
}
function measureExplicitStaticRead(): array
{
StaticSlotData::$counter = 1;
$best = PHP_FLOAT_MAX;
$checksum = 0;
for ($round = 0; $round < STATIC_CACHE_WARMUPS + STATIC_CACHE_ROUNDS; $round++) {
$sum = 0;
$start = hrtime(true);
for ($i = 0; $i < STATIC_CACHE_ITERATIONS; $i++) {
$sum += StaticSlotData::$counter;
}
$elapsed = hrtime(true) - $start;
if ($round >= STATIC_CACHE_WARMUPS && $elapsed < $best) {
$best = $elapsed;
}
$checksum += $sum;
}
return [$best / STATIC_CACHE_ITERATIONS, $checksum];
}
function measureStaticCacheGetData(): array function measureStaticCacheGetData(): array
{ {
$best = PHP_FLOAT_MAX; $best = PHP_FLOAT_MAX;
@ -65,11 +127,21 @@ function main(): void
{ {
StaticCacheData::getData(); StaticCacheData::getData();
[$explicitRead, $explicitReadChecksum] = measureExplicitStaticRead();
StaticSlotData::$counter = 1;
[$selfRead, $selfReadChecksum] = StaticSlotData::measureSelfRead();
[$selfWrite, $selfWriteChecksum] = StaticSlotData::measureSelfWrite();
[$getData, $getDataChecksum] = measureStaticCacheGetData(); [$getData, $getDataChecksum] = measureStaticCacheGetData();
[$getTable, $getTableChecksum] = measureStaticCacheGetTable(); [$getTable, $getTableChecksum] = measureStaticCacheGetTable();
echo "explicit_read_ns={$explicitRead}\n";
echo "self_read_ns={$selfRead}\n";
echo "self_write_ns={$selfWrite}\n";
echo "get_data_ns={$getData}\n"; echo "get_data_ns={$getData}\n";
echo "get_table_ns={$getTable}\n"; echo "get_table_ns={$getTable}\n";
echo "checksum_explicit_read={$explicitReadChecksum}\n";
echo "checksum_self_read={$selfReadChecksum}\n";
echo "checksum_self_write={$selfWriteChecksum}\n";
echo "checksum_get_data={$getDataChecksum}\n"; echo "checksum_get_data={$getDataChecksum}\n";
echo "checksum_get_table={$getTableChecksum}\n"; echo "checksum_get_table={$getTableChecksum}\n";
} }

@ -105,7 +105,7 @@ $typephpResult = parseStaticCacheResult(runStaticCacheCommand([$binary], $root,
echo "Runtime: {$phpRuntime}\n"; echo "Runtime: {$phpRuntime}\n";
echo "Metric PHP ns/op TypePHP ns/op TypePHP/PHP\n"; echo "Metric PHP ns/op TypePHP ns/op TypePHP/PHP\n";
echo "--------------------------------------------------\n"; echo "--------------------------------------------------\n";
foreach (['get_data', 'get_table'] as $case) { foreach (['explicit_read', 'self_read', 'self_write', 'get_data', 'get_table'] as $case) {
$metric = $case . '_ns'; $metric = $case . '_ns';
$checksum = 'checksum_' . $case; $checksum = 'checksum_' . $case;
if (!isset($phpResult[$metric], $typephpResult[$metric])) { if (!isset($phpResult[$metric], $typephpResult[$metric])) {

@ -103,11 +103,11 @@ If a method never uses scoped dynamic calls, first-class callables, or scoped ca
### 3.5 Usage Entry Points ### 3.5 Usage Entry Points
#### `php::callScoped()` #### `typephp_call_method_scoped_cached()`
Used for dynamic function or object method calls. Internally, `call_function_impl()` uses `CallableScope::resolve()` to obtain a `zend_fcall_info_cache`, then executes `zend_call_function()`. Used for dynamic object method calls. It uses `CallableScope::resolve()` to obtain a `zend_fcall_info_cache`, retains cacheable results in a request-local call-site slot, then executes `zend_call_function()`.
The typical scenario is when the compiler cannot resolve an object method into a Native Call, but still needs to preserve access to the current class's private/protected members. The typical scenario is when the compiler cannot resolve an object method into a Native Call, but still needs to preserve access to the current class's private/protected members. A hit must match the target class, method name, lexical scope, called scope, and caller `$this` class. Trampolines and changing call sites continue through full scoped resolution.
#### `php::makeScopedCallable()` #### `php::makeScopedCallable()`
@ -287,9 +287,9 @@ Likewise, `EG(fake_scope)` must not be unconditionally set at the entry of every
```text ```text
AOT method entry AOT method entry
-> lazily generated CallableScope -> lazily generated CallableScope
-> php::callScoped() -> typephp_call_method_scoped_cached()
-> CallableScope::resolve() -> call-site cache hit, or CallableScope::resolve()
-> zend_is_callable_at_frame(synthetic frame) -> zend_is_callable_at_frame(synthetic frame) on cache miss
-> zend_call_function() -> zend_call_function()
``` ```
@ -345,7 +345,7 @@ save EG(fake_scope)
| Path | Main Cost | Optimization Strategy | | Path | Main Cost | Optimization Strategy |
| --- | --- | --- | | --- | --- | --- |
| `CallableScope` | Initializing one synthetic frame | At most once per AOT method, reused across loops | | `CallableScope` | Initializing one synthetic frame | At most once per AOT method, reused across loops |
| `callScoped()` | Dynamic resolution by `zend_is_callable_at_frame()` | Used only for dynamic calls; resolvable Native Calls do not enter this path | | `typephp_call_method_scoped_cached()` | Guarded cache lookup; `zend_is_callable_at_frame()` on a miss | One request-local slot per dynamic call site; resolvable Native Calls do not enter this path |
| `prepareScopedCallback()` | One callable resolution | Public absolute callbacks do not create a Closure | | `prepareScopedCallback()` | One callable resolution | Public absolute callbacks do not create a Closure |
| `makeScopedCallable()` | Callable resolution and Closure allocation | Used only for first-class callables | | `makeScopedCallable()` | Callable resolution and Closure allocation | Used only for first-class callables |
| `UserCodeScopeGuard` | One pointer lookup and write at method entry, plus restoration at exit | Generated only for `call_user_func*`, callback maps, or unresolved unpack callbacks | | `UserCodeScopeGuard` | One pointer lookup and write at method entry, plus restoration at exit | Generated only for `call_user_func*`, callback maps, or unresolved unpack callbacks |

@ -103,11 +103,11 @@ php::CallableScope tmp_var_1 = php::getCallableScope(
### 3.5 使用入口 ### 3.5 使用入口
#### `php::callScoped()` #### `typephp_call_method_scoped_cached()`
用于动态函数或对象方法调用。内部 `call_function_impl()` 使用 `CallableScope::resolve()` 获取 `zend_fcall_info_cache`,然后执行 `zend_call_function()` 用于动态对象方法调用。它使用 `CallableScope::resolve()` 获取 `zend_fcall_info_cache`,将允许缓存的结果保存在 request 级调用点 slot 中,然后执行 `zend_call_function()`
典型场景是编译器无法将对象方法解析为 Native Call,但仍需保留当前类的 private/protected 访问权。 典型场景是编译器无法将对象方法解析为 Native Call,但仍需保留当前类的 private/protected 访问权。缓存命中必须同时匹配目标类、方法名、lexical scope、called scope 和调用方 `$this` 的类;trampoline 或发生变化的调用点仍执行完整 scoped resolution。
#### `php::makeScopedCallable()` #### `php::makeScopedCallable()`
@ -287,9 +287,9 @@ fake_scope_guard.restore();
```text ```text
AOT method entry AOT method entry
-> lazily generated CallableScope -> lazily generated CallableScope
-> php::callScoped() -> typephp_call_method_scoped_cached()
-> CallableScope::resolve() -> 调用点缓存命中,或 CallableScope::resolve()
-> zend_is_callable_at_frame(synthetic frame) -> 缓存未命中时执行 zend_is_callable_at_frame(synthetic frame)
-> zend_call_function() -> zend_call_function()
``` ```
@ -345,7 +345,7 @@ save EG(fake_scope)
| 路径 | 主要成本 | 优化策略 | | 路径 | 主要成本 | 优化策略 |
| --- | --- | --- | | --- | --- | --- |
| `CallableScope` | 初始化一个 synthetic frame | 每个 AOT 方法最多一次,循环复用 | | `CallableScope` | 初始化一个 synthetic frame | 每个 AOT 方法最多一次,循环复用 |
| `callScoped()` | `zend_is_callable_at_frame()` 动态解析 | 仅动态调用使用;可解析的 Native Call 不进入此路径 | | `typephp_call_method_scoped_cached()` | 带 guard 的缓存查找;未命中时执行 `zend_is_callable_at_frame()` | 每个动态调用点一个 request 级 slot;可解析的 Native Call 不进入此路径 |
| `prepareScopedCallback()` | 一次 callable 解析 | public 绝对 callback 不创建 Closure | | `prepareScopedCallback()` | 一次 callable 解析 | public 绝对 callback 不创建 Closure |
| `makeScopedCallable()` | callable 解析及 Closure 分配 | 仅 first-class callable 使用 | | `makeScopedCallable()` | callable 解析及 Closure 分配 | 仅 first-class callable 使用 |
| `UserCodeScopeGuard` | 方法入口一次指针查找、写入和退出恢复 | 只为 `call_user_func*`、callback map 或未解析的 unpack callback 生成 | | `UserCodeScopeGuard` | 方法入口一次指针查找、写入和退出恢复 | 只为 `call_user_func*`、callback map 或未解析的 unpack callback 生成 |

@ -5,9 +5,36 @@ function call_cache_target(int $value): int
return $value + 1; return $value + 1;
} }
function call_cache_sites(mixed $callback, object $object, mixed $method): array function call_cache_sites(mixed $callback, object $object, ?object $nullable, mixed $method): array
{ {
return [$callback(1), $object->$method(2)]; return [
$callback(1),
$object->$method(2),
$object->fixedMethod(3),
$nullable?->nullableMethod(4),
];
}
function call_cache_known_internal_method(ArrayObject $object): int
{
return $object->count();
}
class CallCacheStaticTarget
{
public static function fixedMethod(int $value): int
{
return $value + 1;
}
}
function call_cache_static_sites(mixed $class, mixed $method): array
{
return [
$class::$method(5),
CallCacheStaticTarget::$method(6),
$class::fixedMethod(7),
];
} }
class CallCacheScopedTarget class CallCacheScopedTarget

@ -0,0 +1,27 @@
<?php
class StaticPropertyFunctionLocalCache
{
public static mixed $value = 1;
public static function noStaticProperty(): int
{
return 1;
}
public static function repeatedSelf(): array
{
self::$value = null;
return [self::$value, self::$value];
}
public static function repeatedStatic(): array
{
static::$value = null;
return [static::$value, static::$value, static::class, static::class];
}
}
function main(): void
{
}

@ -22,12 +22,14 @@ final class CallCacheCodegenTest extends BaseTest
self::assertIsString($code); self::assertIsString($code);
self::assertIsString($extension); self::assertIsString($extension);
self::assertSame(1, substr_count($code, 'typephp_call_cached(')); self::assertSame(4, substr_count($code, 'typephp_call_cached('));
self::assertSame(1, substr_count($code, 'typephp_call_method_cached(')); self::assertSame(3, substr_count($code, 'typephp_call_method_cached('));
self::assertStringContainsString('php::callScoped(', $code); self::assertSame(1, substr_count($code, 'typephp_call_method_scoped_cached('));
self::assertStringNotContainsString('php::callScoped(', $code);
self::assertStringContainsString('.call(get_persistent_method(', $code);
self::assertStringContainsString('php::FunctionCallCacheSlot function_call_cache_map[1]', $extension); self::assertStringContainsString('php::FunctionCallCacheSlot function_call_cache_map[4]', $extension);
self::assertStringContainsString('php::MethodCallCacheSlot method_call_cache_map[1]', $extension); self::assertStringContainsString('php::MethodCallCacheSlot method_call_cache_map[4]', $extension);
self::assertStringContainsString('typephp_get_function_call_cache(FunctionCallCacheId cache_id)', $extension); self::assertStringContainsString('typephp_get_function_call_cache(FunctionCallCacheId cache_id)', $extension);
self::assertStringContainsString('typephp_get_method_call_cache(MethodCallCacheId cache_id)', $extension); self::assertStringContainsString('typephp_get_method_call_cache(MethodCallCacheId cache_id)', $extension);
} }

@ -60,6 +60,6 @@ class DevirtualizeOrderTest extends TestCase
// A direct native call to the base implementation means the override // A direct native call to the base implementation means the override
// was wrongly devirtualized; the call must go through dynamic dispatch. // was wrongly devirtualized; the call must go through dynamic dispatch.
self::assertStringNotContainsString('php_ordertest__base__perform', $m['body']); self::assertStringNotContainsString('php_ordertest__base__perform', $m['body']);
self::assertStringContainsString('callScoped', $m['body']); self::assertStringContainsString('typephp_call_method_scoped_cached', $m['body']);
} }
} }

@ -111,7 +111,11 @@ final class LocalVariableInitializerTest extends \BaseTest
self::assertStringContainsString('php::Str unknownClass = get_str(', $code); self::assertStringContainsString('php::Str unknownClass = get_str(', $code);
self::assertStringContainsString('php::Var lateStatic;', $code); self::assertStringContainsString('php::Var lateStatic;', $code);
self::assertStringContainsString('lateStatic = php::constant(typephp_get_called_ce(this_)', $code); self::assertStringContainsString(
'zend_class_entry *const _typephp_called_ce = typephp_get_called_ce(this_);',
$code,
);
self::assertStringContainsString('lateStatic = php::constant(_typephp_called_ce', $code);
self::assertStringContainsString('php::Var external = "', $code); self::assertStringContainsString('php::Var external = "', $code);
self::assertStringNotContainsString("php::Var external;\n", $code); self::assertStringNotContainsString("php::Var external;\n", $code);
self::assertStringContainsString('php::Var runtimeClassConstant;', $code); self::assertStringContainsString('php::Var runtimeClassConstant;', $code);

@ -54,8 +54,8 @@ class LoopControlTest extends \BaseTest
$this->assertMatchesRegularExpression('/\.attr\([^)]+\)[^;]*--/', $cpp); $this->assertMatchesRegularExpression('/\.attr\([^)]+\)[^;]*--/', $cpp);
// static-property postfix must NOT be rewritten // static-property postfix must NOT be rewritten
$this->assertMatchesRegularExpression('/typephp_get_static_property\([^)]+\)[^;]*\+\+/', $cpp); $this->assertMatchesRegularExpression('/_typephp_static_property_\d+\(\)\+\+/', $cpp);
$this->assertMatchesRegularExpression('/typephp_get_static_property\([^)]+\)[^;]*--/', $cpp); $this->assertMatchesRegularExpression('/_typephp_static_property_\d+\(\)--/', $cpp);
// array-element postfix must NOT be rewritten // array-element postfix must NOT be rewritten
$this->assertMatchesRegularExpression('/\.item\([^)]+\)[^;]*\+\+/', $cpp); $this->assertMatchesRegularExpression('/\.item\([^)]+\)[^;]*\+\+/', $cpp);

@ -13,11 +13,11 @@ final class MagicCallCodegenTest extends \BaseTest
self::assertStringNotContainsString('.call(', $exactBody); self::assertStringNotContainsString('.call(', $exactBody);
$runtimeBody = $this->functionBody($code, 'php_runtimemagiccall'); $runtimeBody = $this->functionBody($code, 'php_runtimemagiccall');
self::assertStringContainsString('.call(', $runtimeBody); self::assertStringContainsString('typephp_call_method_cached(', $runtimeBody);
self::assertStringNotContainsString('php_exactmagichandler____call(', $runtimeBody); self::assertStringNotContainsString('php_exactmagichandler____call(', $runtimeBody);
$internalBody = $this->functionBody($code, 'php_exactinternalmethod'); $internalBody = $this->functionBody($code, 'php_exactinternalmethod');
self::assertStringContainsString('.call(', $internalBody); self::assertStringContainsString('typephp_call_method_cached(', $internalBody);
self::assertStringNotContainsString('__call(', $internalBody); self::assertStringNotContainsString('__call(', $internalBody);
} }

@ -38,9 +38,15 @@ class NativePropertyTest extends \BaseTest
} }
$code = file_get_contents($outputFile); $code = file_get_contents($outputFile);
$this->assertStringContainsString('tmp_var_0 = typephp_get_called_class(this_);', $code); $this->assertStringContainsString(
$this->assertStringContainsString('php::getStaticProperty((tmp_var_0.isObject()', $code); 'zend_class_entry *const _typephp_called_ce = typephp_get_called_ce(this_);',
$this->assertStringContainsString('php::getStaticProperty((tmp_var_0.isObject() ? php::fn::get_class(tmp_var_0)', $code); $code,
);
$this->assertStringContainsString(
'typephp_get_static_property_slot(_typephp_called_ce, get_str(',
$code,
);
$this->assertStringNotContainsString('typephp_get_called_class(this_)', $code);
$this->assertStringContainsString('= php::toInt(value);', $code); $this->assertStringContainsString('= php::toInt(value);', $code);
} }

@ -0,0 +1,54 @@
<?php
use TypePhp\CompilerTest;
use TypePhp\Exception\TestError;
class StaticPropertyFunctionLocalCacheTest extends BaseTest
{
public function testSlotsAndCalledScopeAreGeneratedOnlyForFunctionsThatUseThem(): void
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/static-property-function-local-cache.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
try {
$output = $compiler->convertFile($source);
} catch (TestError $error) {
self::fail($error->getMessage());
}
$code = file_get_contents($output);
self::assertIsString($code);
self::assertMatchesRegularExpression(
'/php_staticpropertyfunctionlocalcache__nostaticproperty\(.*?return php::toInt\(1L\);\n}/s',
$code,
);
preg_match(
'/php_staticpropertyfunctionlocalcache__nostaticproperty\(.*?\n}/s',
$code,
$noStatic,
);
self::assertStringNotContainsString('_typephp_static_property_slot_', $noStatic[0]);
self::assertStringNotContainsString('_typephp_called_ce', $noStatic[0]);
preg_match('/php_staticpropertyfunctionlocalcache__repeatedself\(.*?\n}/s', $code, $selfMethod);
self::assertSame(1, substr_count($selfMethod[0], 'zval *_typephp_static_property_slot_0 = nullptr;'));
self::assertSame(1, substr_count($selfMethod[0], 'const auto _typephp_static_property_0'));
self::assertSame(1, substr_count($selfMethod[0], 'typephp_get_static_property_cached('));
self::assertSame(3, substr_count($selfMethod[0], '_typephp_static_property_0()'));
self::assertSame(1, substr_count($selfMethod[0], 'typephp_get_static_property_slot(get_persistent_class('));
self::assertStringNotContainsString('_typephp_called_ce', $selfMethod[0]);
preg_match('/php_staticpropertyfunctionlocalcache__repeatedstatic\(.*?\n}/s', $code, $staticMethod);
self::assertSame(1, substr_count($staticMethod[0], 'typephp_get_called_ce(this_)'));
self::assertSame(1, substr_count($staticMethod[0], 'typephp_get_called_class(_typephp_called_ce)'));
self::assertSame(1, substr_count($staticMethod[0], 'zval *_typephp_static_property_slot_0 = nullptr;'));
self::assertSame(1, substr_count($staticMethod[0], 'const auto _typephp_static_property_0'));
self::assertSame(1, substr_count($staticMethod[0], 'typephp_get_static_property_cached('));
self::assertSame(3, substr_count($staticMethod[0], '_typephp_static_property_0()'));
self::assertSame(1, substr_count($staticMethod[0], 'typephp_get_static_property_slot(_typephp_called_ce'));
}
}

@ -1327,6 +1327,21 @@ class CompilerBase implements PropertyAccessContext
return 'typephp_get_function_call_cache(FunctionCallCacheId{' . $id . '})'; return 'typephp_get_function_call_cache(FunctionCallCacheId{' . $id . '})';
} }
/** Return the function-local late-static-bound class entry. */
protected function getCalledCeExpr(): string
{
$this->context->needsCalledCe = true;
return '_typephp_called_ce';
}
/** Return the function-local late-static-bound class name. */
protected function getCalledClassExpr(): string
{
$this->context->needsCalledCe = true;
$this->context->needsCalledClass = true;
return '_typephp_called_class';
}
protected function getClassEntryPtr(string $className): string protected function getClassEntryPtr(string $className): string
{ {
$id = $this->getClassId($className); $id = $this->getClassId($className);
@ -3914,7 +3929,7 @@ class CompilerBase implements PropertyAccessContext
if ($this->classDef?->nativeObject) { if ($this->classDef?->nativeObject) {
$this->fatalError($expr, 'Native classes do not support `new static()`'); $this->fatalError($expr, 'Native classes do not support `new static()`');
} }
$cePtr = Symbol::getCalledCe(); $cePtr = $this->getCalledCeExpr();
} else { } else {
if ($className === 'self') { if ($className === 'self') {
$className = $this->getFullClassName(); $className = $this->getFullClassName();
@ -4112,7 +4127,7 @@ class CompilerBase implements PropertyAccessContext
if (!$this->classDef) { if (!$this->classDef) {
$this->fatalError($class, 'Cannot use "static" outside a class'); $this->fatalError($class, 'Cannot use "static" outside a class');
} }
return Symbol::getCalledCe(); return $this->getCalledCeExpr();
} else { } else {
$className = $this->getNamespacedClassName($className); $className = $this->getNamespacedClassName($className);
} }
@ -4733,7 +4748,7 @@ class CompilerBase implements PropertyAccessContext
if ($id === 'self') { if ($id === 'self') {
$id = $this->getFullClassName(); $id = $this->getFullClassName();
} elseif ($id === 'static') { } elseif ($id === 'static') {
return Symbol::getCalledClass(); return $this->getCalledClassExpr();
} }
if ($this->isNameExpr($node) or $this->isIdExpr($node)) { if ($this->isNameExpr($node) or $this->isIdExpr($node)) {
return $literal ? $this->getLiteralString($id) : $this->genCharPtr($id, true); return $literal ? $this->getLiteralString($id) : $this->genCharPtr($id, true);
@ -5220,6 +5235,16 @@ class CompilerBase implements PropertyAccessContext
. $this->getMethodPtr($this->getFullClassName(), $this->methodDef->name) . $this->getMethodPtr($this->getFullClassName(), $this->methodDef->name)
. ', this_);' . PHP_EOL; . ', this_);' . PHP_EOL;
} }
if ($this->context->needsCalledCe) {
$code .= $this->getIndent()
. 'zend_class_entry *const _typephp_called_ce = typephp_get_called_ce(this_);'
. PHP_EOL;
}
if ($this->context->needsCalledClass) {
$code .= $this->getIndent()
. 'const php::Str _typephp_called_class = typephp_get_called_class(_typephp_called_ce);'
. PHP_EOL;
}
$code .= $this->genLocalVarDecl($this->context->localVars); $code .= $this->genLocalVarDecl($this->context->localVars);
foreach ($this->context->classEntryPtrs as $className => $entry) { foreach ($this->context->classEntryPtrs as $className => $entry) {
$code .= $this->getIndent() . 'zend_class_entry *' . $entry . ' = ' $code .= $this->getIndent() . 'zend_class_entry *' . $entry . ' = '
@ -5267,13 +5292,11 @@ class CompilerBase implements PropertyAccessContext
$code .= $this->getIndent() . $info['type'] . ' &' . $name . ' = ' . $zvalMacro . '(' . $info['getter'] . '.unwrap_ptr());' . PHP_EOL; $code .= $this->getIndent() . $info['type'] . ' &' . $name . ' = ' . $zvalMacro . '(' . $info['getter'] . '.unwrap_ptr());' . PHP_EOL;
} }
} }
foreach ($this->context->staticPropRefs as $name => $info) { foreach ($this->context->staticPropRefs as $info) {
$getter = Symbol::getStaticProperty() . '(' . $info['classPtr'] . ', ' . $info['offsetExpr'] . ')'; $code .= $this->getIndent() . 'zval *' . $info['name'] . ' = nullptr;' . PHP_EOL;
if (($info['kind'] ?? 'zval') === 'var') { $code .= $this->getIndent() . 'const auto ' . $info['accessorName'] . ' = [&]() {'
$code .= $this->getIndent() . Type::VAR . ' ' . $name . ' = ' . $getter . ';' . PHP_EOL; . ' return typephp_get_static_property_cached(' . $info['name'] . ', [&]() {'
} else { . ' return ' . $info['resolver'] . '; }); };' . PHP_EOL;
$code .= $this->getIndent() . 'zval *' . $name . ' = ' . $getter . '.unwrap_ptr();' . PHP_EOL;
}
} }
return $code; return $code;
} }

@ -83,6 +83,10 @@ class FunctionContext
public array $classEntryPtrs = []; public array $classEntryPtrs = [];
/** Reusable php::CallableScope local, created only when this function performs scoped calls. */ /** Reusable php::CallableScope local, created only when this function performs scoped calls. */
public ?string $callableScopeVar = null; public ?string $callableScopeVar = null;
/** This function reads the late-static-bound class entry. */
public bool $needsCalledCe = false;
/** This function reads the late-static-bound class name. */
public bool $needsCalledClass = false;
/** This generated body needs a temporary lexical scope on the nearest user-code frame. */ /** This generated body needs a temporary lexical scope on the nearest user-code frame. */
public bool $needsUserCodeCallableScope = false; public bool $needsUserCodeCallableScope = false;
public int $tmpVarIndex = 0; public int $tmpVarIndex = 0;
@ -112,7 +116,7 @@ class FunctionContext
public array $beforeStmtLines = []; public array $beforeStmtLines = [];
public array $afterStmtLines = []; public array $afterStmtLines = [];
public array $objectProps; public array $objectProps;
/** Map of static property local slots. int/float keep stable zval* slots; other types use Var slots. */ /** Map of lazily resolved, function-local static-property zval slots. */
public array $staticPropRefs = []; public array $staticPropRefs = [];
public int $scopeLevel = 0; public int $scopeLevel = 0;
/** /**
@ -145,6 +149,8 @@ class FunctionContext
$this->ceWrappers = []; $this->ceWrappers = [];
$this->classEntryPtrs = []; $this->classEntryPtrs = [];
$this->callableScopeVar = null; $this->callableScopeVar = null;
$this->needsCalledCe = false;
$this->needsCalledClass = false;
$this->tmpVarIndex = 0; $this->tmpVarIndex = 0;
$this->scopeLayouts = []; $this->scopeLayouts = [];
$this->callableScopeVar = null; $this->callableScopeVar = null;
@ -188,6 +194,8 @@ class FunctionContext
$this->hoistedProps = []; $this->hoistedProps = [];
$this->staticPropRefs = []; $this->staticPropRefs = [];
$this->classEntryPtrs = []; $this->classEntryPtrs = [];
$this->needsCalledCe = false;
$this->needsCalledClass = false;
$this->scopeLayouts = []; $this->scopeLayouts = [];
$this->scopeLevel = 0; $this->scopeLevel = 0;
$this->inLoop = false; $this->inLoop = false;

@ -31,7 +31,7 @@ trait ClosureGenerator
// PHP flattens a trait method into the consuming class. A closure // PHP flattens a trait method into the consuming class. A closure
// declared in that method therefore uses the consuming class as // declared in that method therefore uses the consuming class as
// its lexical scope, never the trait's own class entry. // its lexical scope, never the trait's own class entry.
$scope = 'php::getCalledCe(this_)'; $scope = $this->getCalledCeExpr();
} else { } else {
$scope = $this->class $scope = $this->class
? $this->getClassEntryPtr($this->getFullClassName()) ? $this->getClassEntryPtr($this->getFullClassName())

@ -291,7 +291,7 @@ trait TypeCheckGenerator
'iterable' => '(' . $v . '.isArray() || (' . $v . '.isObject() && php::instanceOf(' . $v . ', zend_ce_traversable)))', 'iterable' => '(' . $v . '.isArray() || (' . $v . '.isObject() && php::instanceOf(' . $v . ', zend_ce_traversable)))',
'allOf' => $this->genAllOfTypeCondition($varName, $entry['types']), 'allOf' => $this->genAllOfTypeCondition($varName, $entry['types']),
'instanceof' => $entry['class'] === 'static' 'instanceof' => $entry['class'] === 'static'
? '(' . $v . '.isObject() && php::instanceOf(' . $v . ', php::getCalledCe(this_)))' ? '(' . $v . '.isObject() && php::instanceOf(' . $v . ', ' . $this->getCalledCeExpr() . '))'
: '(' . $v . '.isObject() && php::instanceOf(' . $v . ', ' . $this->getClassEntryPtr($entry['class']) . '))', : '(' . $v . '.isObject() && php::instanceOf(' . $v . ', ' . $this->getClassEntryPtr($entry['class']) . '))',
default => '', default => '',
}; };

@ -139,9 +139,9 @@ trait ClassConstantFetchTrait
$this->fatalError($expr, "The 'static' keyword can only be used as the class name in class methods"); $this->fatalError($expr, "The 'static' keyword can only be used as the class name in class methods");
} }
if ($const === 'class') { if ($const === 'class') {
return Symbol::getCalledClass(); return $this->getCalledClassExpr();
} else { } else {
return Symbol::constant() . '(' . Symbol::getCalledCe() . ', ' . $this->getLiteralString($const) . ')'; return Symbol::constant() . '(' . $this->getCalledCeExpr() . ', ' . $this->getLiteralString($const) . ')';
} }
} }
@ -241,7 +241,7 @@ trait ClassConstantFetchTrait
if (!$this->methodDef) { if (!$this->methodDef) {
$this->fatalError($expr, "The 'static' keyword can only be used as the class name in class methods"); $this->fatalError($expr, "The 'static' keyword can only be used as the class name in class methods");
} }
$ce = Symbol::getCalledCe(); $ce = $this->getCalledCeExpr();
} elseif ($class === 'self' or $class === 'this_') { } elseif ($class === 'self' or $class === 'this_') {
$ce = $this->getClassEntryPtr($this->getFullClassName()); $ce = $this->getClassEntryPtr($this->getFullClassName());
} elseif ($class === 'parent') { } elseif ($class === 'parent') {

@ -161,7 +161,7 @@ trait ConstantExpressionTrait
$this->fatalError($expr, 'The magic constant `__CLASS__` is not allowed in global scope'); $this->fatalError($expr, 'The magic constant `__CLASS__` is not allowed in global scope');
} }
if ($this->classDef->trait) { if ($this->classDef->trait) {
return Symbol::getCalledClass(); return $this->getCalledClassExpr();
} }
return '"' . $this->escapeString($class) . '"'; return '"' . $this->escapeString($class) . '"';
case 'Scalar_MagicConst_Trait': case 'Scalar_MagicConst_Trait':

@ -36,7 +36,8 @@ trait FunctionCallTrait
} }
$callable = $this->parseExprAsValue($expr->right); $callable = $this->parseExprAsValue($expr->right);
return 'php::call(' . $callable . ', {' . $value . '})'; return 'typephp_call_cached(' . $callable . ', ' . $this->getFunctionCallCache()
. ', {' . $value . '})';
} }
/** /**

@ -487,7 +487,7 @@ trait MethodCallTrait
$staticCall = (bool) ($this->methodDef->flags & Modifiers::STATIC); $staticCall = (bool) ($this->methodDef->flags & Modifiers::STATIC);
} }
if ($staticCall) { if ($staticCall) {
$callable = Symbol::getCalledCe() . ', ' . $methodPtr; $callable = $this->getCalledCeExpr() . ', ' . $methodPtr;
if (empty($expr->args)) { if (empty($expr->args)) {
return 'php::call(' . $callable . ')'; return 'php::call(' . $callable . ')';
} }
@ -847,9 +847,11 @@ trait MethodCallTrait
$magicMethod, $magicMethod,
$this->isVarExpr($expr->var) && $this->parseIdentifier($expr->var) === 'this_', $this->isVarExpr($expr->var) && $this->parseIdentifier($expr->var) === 'this_',
); );
$resolvedMethodPtr = false;
if ($class && $funcName && !$magicMethod) { if ($class && $funcName && !$magicMethod) {
if ($this->isInternalClass($class)) { if ($this->isInternalClass($class)) {
$methodPtr = $this->getMethodPtr($class, $funcName); $methodPtr = $this->getMethodPtr($class, $funcName);
$resolvedMethodPtr = true;
} else { } else {
$methodPtr = $method; $methodPtr = $method;
} }
@ -859,9 +861,13 @@ trait MethodCallTrait
if (empty($expr->args)) { if (empty($expr->args)) {
if ($requiresDynamicScope && $this->methodDef) { if ($requiresDynamicScope && $this->methodDef) {
if (!$resolvedMethodPtr) {
return 'typephp_call_method_scoped_cached(' . $object . ', ' . $methodPtr . ', '
. $this->getCallableScopeExpr() . ', ' . $this->getMethodCallCache() . ')';
}
return 'php::callScoped(' . $object . ', ' . $methodPtr . ', ' . $this->getCallableScopeExpr() . ')'; return 'php::callScoped(' . $object . ', ' . $methodPtr . ', ' . $this->getCallableScopeExpr() . ')';
} }
if (!$this->isNamedMethod($expr->name)) { if (!$resolvedMethodPtr) {
return 'typephp_call_method_cached(' . $object . ', ' . $methodPtr . ', ' return 'typephp_call_method_cached(' . $object . ', ' . $methodPtr . ', '
. $this->getMethodCallCache() . ')'; . $this->getMethodCallCache() . ')';
} }
@ -869,9 +875,15 @@ trait MethodCallTrait
} }
try { try {
$class = empty($class) ? self::DYNAMIC_CALLED_CLASS : $class; $class = empty($class) ? self::DYNAMIC_CALLED_CLASS : $class;
if (!$this->isNamedMethod($expr->name) && !($requiresDynamicScope && $this->methodDef)) { if (!$resolvedMethodPtr) {
$callArgs = $this->parseCallArgs($expr->args, $funcName, $class);
if ($requiresDynamicScope && $this->methodDef) {
return 'typephp_call_method_scoped_cached(' . $object . ', ' . $methodPtr . ', '
. $this->getCallableScopeExpr() . ', ' . $this->getMethodCallCache() . ', '
. $callArgs . ')';
}
return 'typephp_call_method_cached(' . $object . ', ' . $methodPtr . ', ' return 'typephp_call_method_cached(' . $object . ', ' . $methodPtr . ', '
. $this->getMethodCallCache() . ', ' . $this->parseCallArgs($expr->args) . ')'; . $this->getMethodCallCache() . ', ' . $callArgs . ')';
} }
return $this->genRuntimeObjectMethodCall( return $this->genRuntimeObjectMethodCall(
$object, $object,
@ -994,7 +1006,7 @@ trait MethodCallTrait
return null; return null;
} }
$calledCe = Symbol::getCalledCe(); $calledCe = $this->getCalledCeExpr();
$direct = 'php::Var(' . self::PREFIX . $nativeFunc . '(this_))'; $direct = 'php::Var(' . self::PREFIX . $nativeFunc . '(this_))';
$fallback = 'php::call(' . $calledCe . ', php::getMethod(' . $calledCe . ', ' . $methodPtr . '))'; $fallback = 'php::call(' . $calledCe . ', php::getMethod(' . $calledCe . ', ' . $methodPtr . '))';
return '(EXPECTED(' . $calledCe . ' == ' . $this->getClassEntryPtr($class) . ')' return '(EXPECTED(' . $calledCe . ' == ' . $this->getClassEntryPtr($class) . ')'
@ -1020,6 +1032,7 @@ trait MethodCallTrait
$callScope = []; $callScope = [];
$rtFunc = ''; $rtFunc = '';
$rtClass = ''; $rtClass = '';
$cacheCallable = false;
$canUseDirectCallScope = $this->isNameExpr($expr->class) && $this->isIdExpr($expr->name); $canUseDirectCallScope = $this->isNameExpr($expr->class) && $this->isIdExpr($expr->name);
$class = ($this->isNameExpr($expr->class) || $this->isVarExpr($expr->class)) $class = ($this->isNameExpr($expr->class) || $this->isVarExpr($expr->class))
? $this->parseIdentifier($expr->class) ? $this->parseIdentifier($expr->class)
@ -1061,9 +1074,11 @@ trait MethodCallTrait
} }
} }
$placeHolder = $fn; $placeHolder = $fn;
$cacheCallable = true;
} elseif ($this->isVarExpr($expr->name)) { } elseif ($this->isVarExpr($expr->name)) {
$fn = 'php::concat({' . $this->identifierToStr($expr->class) . ', "::", ' . $this->methodNameToStr($expr->name) . '})'; $fn = 'php::concat({' . $this->identifierToStr($expr->class) . ', "::", ' . $this->methodNameToStr($expr->name) . '})';
$placeHolder = $fn; $placeHolder = $fn;
$cacheCallable = true;
} elseif ($class === 'static') { } elseif ($class === 'static') {
if ($this->classDef?->nativeObject) { if ($this->classDef?->nativeObject) {
$this->fatalError( $this->fatalError(
@ -1077,14 +1092,15 @@ trait MethodCallTrait
if ($exactCall !== null) { if ($exactCall !== null) {
return $exactCall; return $exactCall;
} }
$fn = Symbol::getCalledCe() . ', php::getMethod(' . Symbol::getCalledCe() . ', ' . $methodPtr . ')'; $calledCe = $this->getCalledCeExpr();
$fn = $calledCe . ', php::getMethod(' . $calledCe . ', ' . $methodPtr . ')';
if ($this->debug) { if ($this->debug) {
$this->context->beforeStmtLines[] = $this->formatCppLineComment( $this->context->beforeStmtLines[] = $this->formatCppLineComment(
'Static Method Call: ', 'Static Method Call: ',
'static::' . $method . '()' 'static::' . $method . '()'
); );
} }
$placeHolder = $this->genArray([Symbol::getCalledClass(), $methodPtr]); $placeHolder = $this->genArray([$this->getCalledClassExpr(), $methodPtr]);
// Used to resolve the method signature when detecting by-reference arguments (late static binding is resolved within the current class hierarchy) // Used to resolve the method signature when detecting by-reference arguments (late static binding is resolved within the current class hierarchy)
$rtFunc = $method; $rtFunc = $method;
$rtClass = $this->getFullClassName(); $rtClass = $this->getFullClassName();
@ -1150,13 +1166,20 @@ trait MethodCallTrait
// reusable handlers and never stores transient trampolines. // reusable handlers and never stores transient trampolines.
$fn = $this->getLiteralString($class . '::' . $method); $fn = $this->getLiteralString($class . '::' . $method);
$placeHolder = $this->genArray($callScope); $placeHolder = $this->genArray($callScope);
$cacheCallable = true;
} }
$call = 'php::call';
if (empty($expr->args)) { if (empty($expr->args)) {
return $call . '(' . $fn . ')'; if ($cacheCallable) {
return 'typephp_call_cached(' . $fn . ', ' . $this->getFunctionCallCache() . ')';
}
return 'php::call(' . $fn . ')';
} }
try { try {
if ($cacheCallable) {
return 'typephp_call_cached(' . $fn . ', ' . $this->getFunctionCallCache() . ', '
. $this->parseCallArgs($expr->args, $rtFunc, $rtClass) . ')';
}
return $this->genRuntimeFunctionCall($fn, $expr->args, $rtFunc, $rtClass); return $this->genRuntimeFunctionCall($fn, $expr->args, $rtFunc, $rtClass);
} catch (PlaceHolder) { } catch (PlaceHolder) {
return $this->genPlaceHolder($placeHolder); return $this->genPlaceHolder($placeHolder);

@ -119,10 +119,12 @@ trait NullsafeAccessTrait
} }
if ($requiresDynamicScope && $this->methodDef) { if ($requiresDynamicScope && $this->methodDef) {
$code .= $this->getIndent() $code .= $this->getIndent()
. "{$tmpVar} = php::callScoped({$object}, {$item[1]}, " . "{$tmpVar} = typephp_call_method_scoped_cached({$object}, {$item[1]}, "
. $this->getCallableScopeExpr() . ", {$args});" . PHP_EOL; . $this->getCallableScopeExpr() . ', ' . $this->getMethodCallCache()
. ", {$args});" . PHP_EOL;
} else { } else {
$code .= $this->getIndent() . "{$tmpVar} = {$object}.call({$item[1]}, {$args});" . PHP_EOL; $code .= $this->getIndent() . "{$tmpVar} = typephp_call_method_cached({$object}, {$item[1]}, "
. $this->getMethodCallCache() . ", {$args});" . PHP_EOL;
} }
if ($argAfterStmts) { if ($argAfterStmts) {
$code .= $this->formatCapturedStmtLines($argAfterStmts); $code .= $this->formatCapturedStmtLines($argAfterStmts);

@ -419,7 +419,7 @@ trait PropertyAccessTrait
} }
if ($resolution->expression !== null) { if ($resolution->expression !== null) {
// Dynamic target, e.g. `self` resolved through the called class inside a trait. // Dynamic target, e.g. `self` resolved through the called class inside a trait.
return Symbol::getStaticPropertyRef() . '(' . Symbol::getCalledCe() . ', ' . $property . ')'; return Symbol::getStaticPropertyRef() . '(' . $this->getCalledCeExpr() . ', ' . $property . ')';
} }
} }
@ -462,7 +462,7 @@ trait PropertyAccessTrait
} }
if ($class === 'self') { if ($class === 'self') {
if ($this->classDef->trait) { if ($this->classDef->trait) {
$expression = Symbol::getStaticProperty() . '(' . Symbol::getCalledCe() . ', ' . $this->getLiteralString($propertyName) . ')'; $expression = Symbol::getStaticProperty() . '(' . $this->getCalledCeExpr() . ', ' . $this->getLiteralString($propertyName) . ')';
return new StaticPropertyFetchTarget($propertyName, null, $expression); return new StaticPropertyFetchTarget($propertyName, null, $expression);
} }
return new StaticPropertyFetchTarget($propertyName, $this->getFullClassName(), null); return new StaticPropertyFetchTarget($propertyName, $this->getFullClassName(), null);
@ -480,6 +480,29 @@ trait PropertyAccessTrait
protected function parseNativeStaticPropertyFetch(Expr\StaticPropertyFetch $expr): ?string protected function parseNativeStaticPropertyFetch(Expr\StaticPropertyFetch $expr): ?string
{ {
if ($this->isNameExpr($expr->class)
&& $this->parseIdentifier($expr->class) === 'static'
&& $this->isIdExpr($expr->name)
) {
if ($this->classDef?->nativeObject) {
$this->fatalError(
$expr,
'Native classes do not support late static binding; use `self::` or a concrete class name',
);
}
if (!$this->methodDef) {
$this->fatalError($expr, "The 'static' keyword can only be used as the class name in class methods");
}
$propertyName = $this->parseIdentifier($expr->name);
$slot = $this->registerStaticPropertySlot(
'static::$' . $propertyName,
'typephp_get_static_property_slot(' . $this->getCalledCeExpr() . ', '
. $this->getLiteralString($propertyName) . ')',
);
$this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_DYNAMIC);
return $slot;
}
$resolution = $this->resolveNativeStaticPropertyFetch($expr); $resolution = $this->resolveNativeStaticPropertyFetch($expr);
if ($resolution !== null) { if ($resolution !== null) {
$nativeProp = $resolution->expression; $nativeProp = $resolution->expression;
@ -492,8 +515,25 @@ trait PropertyAccessTrait
if ($resolution->nativeProperty && $class !== null) { if ($resolution->nativeProperty && $class !== null) {
$classPtr = $this->getClassEntryPtr($class); $classPtr = $this->getClassEntryPtr($class);
$property = $this->parseIdentifier($expr->name);
$slot = $this->registerStaticPropertySlot(
$class . '::$' . $property,
'typephp_get_static_property_slot(' . $classPtr . ', ' . $nativeProp . ')',
);
$this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_DYNAMIC);
return $slot;
} elseif ($resolution->expression !== null && $this->isIdExpr($expr->name)) {
// A trait's self::$property binds to the consuming class. The
// called CE is stable for this function invocation, just like
// static::$property, but not across separate invocations.
$propertyName = $this->parseIdentifier($expr->name);
$slot = $this->registerStaticPropertySlot(
'trait-self::$' . $propertyName,
'typephp_get_static_property_slot(' . $this->getCalledCeExpr() . ', '
. $this->getLiteralString($propertyName) . ')',
);
$this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_DYNAMIC); $this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_DYNAMIC);
return Symbol::getResolvedStaticProperty() . '(' . $classPtr . ', ' . $nativeProp . ')'; return $slot;
} else { } else {
$this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_DYNAMIC); $this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_DYNAMIC);
return $nativeProp; return $nativeProp;
@ -510,29 +550,39 @@ trait PropertyAccessTrait
): string { ): string {
$info = $this->getHoistedObjectPropInfo($def->type); $info = $this->getHoistedObjectPropInfo($def->type);
$propName = $this->parseIdentifier($expr->name); $propName = $this->parseIdentifier($expr->name);
$refVar = '_static_' . str_replace('\\', '_', $class) . '_' . $propName; $classPtr = $this->getClassEntryPtr($class);
$this->registerStaticPropertyRef($refVar, $class, $nativeProp, $info); $slot = $this->registerStaticPropertySlot(
$class . '::$' . $propName,
'typephp_get_static_property_slot(' . $classPtr . ', ' . $nativeProp . ')',
);
if ($info['kind'] === 'zval') { if ($info['kind'] === 'zval') {
$helper = $def->type === Type::FLOAT ? 'typephp_static_float_ref' : 'typephp_static_int_ref'; $helper = $def->type === Type::FLOAT ? 'typephp_static_float_ref' : 'typephp_static_int_ref';
return $helper . '(' . $refVar . ')'; return $helper . '(' . $slot . '.direct_ptr())';
} }
return $refVar; return $slot;
} }
private function registerStaticPropertyRef(string $refVar, string $class, string $offsetExpr, array $info): void /**
* Cache only the raw Zend static-property slot in the current C++ function
* invocation. Its value and reference/indirect state remain live and are
* deliberately re-read on every use.
*/
private function registerStaticPropertySlot(string $key, string $resolver): string
{ {
if (isset($this->context->staticPropRefs[$refVar])) { if (isset($this->context->staticPropRefs[$key])) {
return; return $this->context->staticPropRefs[$key]['accessorName'] . '()';
} }
$this->context->staticPropRefs[$refVar] = [ $name = '_typephp_static_property_slot_' . count($this->context->staticPropRefs);
'type' => $info['type'], $accessorName = '_typephp_static_property_' . count($this->context->staticPropRefs);
'classPtr' => $this->getClassEntryPtr($class), $this->context->staticPropRefs[$key] = [
'offsetExpr' => $offsetExpr, 'name' => $name,
'kind' => $info['kind'], 'accessorName' => $accessorName,
'resolver' => $resolver,
]; ];
return $accessorName . '()';
} }
protected function parseStaticPropertyFetch(Expr\StaticPropertyFetch $expr): string protected function parseStaticPropertyFetch(Expr\StaticPropertyFetch $expr): string
@ -598,7 +648,7 @@ trait PropertyAccessTrait
if (!$this->methodDef) { if (!$this->methodDef) {
$this->fatalError($class, "The 'static' keyword can only be used as the class name in class methods"); $this->fatalError($class, "The 'static' keyword can only be used as the class name in class methods");
} }
return Symbol::getCalledClass(); return $this->getCalledClassExpr();
} }
return $this->getLiteralString($this->getNamespacedClassName($name)); return $this->getLiteralString($this->getNamespacedClassName($name));

@ -45,6 +45,35 @@ class CachedStaticMethod
} }
} }
class CachedStaticMethodSecond
{
public static function run(int $value): string
{
return 'static-second:' . $value;
}
}
class CachedStaticMagic
{
public static function __callStatic(string $name, array $arguments): string
{
return 'static-magic-' . $name . ':' . $arguments[0];
}
}
class CachedScopedMethod
{
private function hidden(int $value): string
{
return 'scoped:' . $value;
}
public function invoke(mixed $method, int $value): string
{
return $this->$method($value);
}
}
function invoke_function(mixed $callback, int $value): string function invoke_function(mixed $callback, int $value): string
{ {
return $callback($value); return $callback($value);
@ -55,6 +84,36 @@ function invoke_method(object $object, mixed $method, int $value): string
return $object->$method($value); return $object->$method($value);
} }
function invoke_named_method(object $object, int $value): string
{
return $object->run($value);
}
function invoke_named_magic(object $object, int $value): string
{
return $object->missing($value);
}
function invoke_nullsafe_named_method(?object $object, int $value): ?string
{
return $object?->run($value);
}
function invoke_static_method(mixed $class, mixed $method, int $value): string
{
return $class::$method($value);
}
function invoke_static_named_method(mixed $class, int $value): string
{
return $class::run($value);
}
function invoke_named_class_dynamic_method(mixed $method, int $value): string
{
return CachedStaticMethod::$method($value);
}
function main(): void function main(): void
{ {
$callbacks = ['cached_first', 'cached_second', 'cached_first']; $callbacks = ['cached_first', 'cached_second', 'cached_first'];
@ -72,6 +131,24 @@ function main(): void
foreach ($objects as $index => $object) { foreach ($objects as $index => $object) {
var_dump(invoke_method($object, $index === 2 ? 'missing' : 'run', $index + 5)); var_dump(invoke_method($object, $index === 2 ? 'missing' : 'run', $index + 5));
} }
var_dump(invoke_named_method($objects[0], 8));
var_dump(invoke_named_method($objects[1], 9));
var_dump(invoke_named_magic($objects[2], 10));
var_dump(invoke_nullsafe_named_method($objects[0], 11));
var_dump(invoke_nullsafe_named_method(null, 12));
var_dump(invoke_nullsafe_named_method($objects[1], 13));
$scoped = new CachedScopedMethod();
var_dump($scoped->invoke('hidden', 14));
var_dump($scoped->invoke('hidden', 15));
var_dump(invoke_static_method('CachedStaticMethod', 'run', 16));
var_dump(invoke_static_method('CachedStaticMethodSecond', 'run', 17));
var_dump(invoke_static_method('CachedStaticMagic', 'missing', 18));
var_dump(invoke_static_named_method('CachedStaticMethod', 19));
var_dump(invoke_named_class_dynamic_method('run', 20));
} }
?> ?>
--EXPECT-- --EXPECT--
@ -83,3 +160,16 @@ string(9) "closure:4"
string(14) "method-first:5" string(14) "method-first:5"
string(15) "method-second:6" string(15) "method-second:6"
string(15) "magic-missing:7" string(15) "magic-missing:7"
string(14) "method-first:8"
string(15) "method-second:9"
string(16) "magic-missing:10"
string(15) "method-first:11"
NULL
string(16) "method-second:13"
string(9) "scoped:14"
string(9) "scoped:15"
string(9) "static:16"
string(16) "static-second:17"
string(23) "static-magic-missing:18"
string(9) "static:19"
string(9) "static:20"

@ -0,0 +1,89 @@
--TEST--
Static-property slot caches retain live values, references, inheritance and late static binding
--FILE--
<?php
class StaticSlotBase
{
public static mixed $shared = 'base';
public static mixed $separate = 'base';
public static function mutateShared(): array
{
$before = self::$shared;
self::$shared = null;
$afterNull = self::$shared;
$reference = &self::$shared;
$reference = 'reference';
return [$before, $afterNull, self::$shared];
}
public static function mutateLate(string $value): array
{
$before = static::$separate;
static::$separate = null;
$afterNull = static::$separate;
static::$separate = $value;
return [static::class, $before, $afterNull, static::$separate];
}
}
class StaticSlotInherited extends StaticSlotBase
{
}
class StaticSlotChildA extends StaticSlotBase
{
public static mixed $separate = 'a';
}
class StaticSlotChildB extends StaticSlotBase
{
public static mixed $separate = 'b';
}
function main(): void
{
var_dump(StaticSlotBase::mutateShared());
var_dump(StaticSlotInherited::$shared);
StaticSlotInherited::$shared = 'child-write';
var_dump(StaticSlotBase::$shared);
var_dump(StaticSlotChildA::mutateLate('A'));
var_dump(StaticSlotChildB::mutateLate('B'));
var_dump(StaticSlotChildA::$separate, StaticSlotChildB::$separate);
}
?>
--EXPECT--
array(3) {
[0]=>
string(4) "base"
[1]=>
NULL
[2]=>
&string(9) "reference"
}
string(9) "reference"
string(11) "child-write"
array(4) {
[0]=>
string(16) "StaticSlotChildA"
[1]=>
string(1) "a"
[2]=>
NULL
[3]=>
string(1) "A"
}
array(4) {
[0]=>
string(16) "StaticSlotChildB"
[1]=>
string(1) "b"
[2]=>
NULL
[3]=>
string(1) "B"
}
string(1) "A"
string(1) "B"
Loading…
Cancel
Save