diff --git a/benchmark/dynamic-call/README.md b/benchmark/dynamic-call/README.md index 7c506365..b5bad1da 100644 --- a/benchmark/dynamic-call/README.md +++ b/benchmark/dynamic-call/README.md @@ -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 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 arguments. This separates callable-cache lookup cost from argument materialization cost and protects the small stack-argument fast path. diff --git a/benchmark/dynamic-call/benchmark.php b/benchmark/dynamic-call/benchmark.php index 9741f207..3e270c72 100644 --- a/benchmark/dynamic-call/benchmark.php +++ b/benchmark/dynamic-call/benchmark.php @@ -82,6 +82,11 @@ final class DynamicCallTarget return $value + 2; } + public function hitZero(): int + { + return 1; + } + public function __invoke(int $value): int { 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 { $sum = 0; @@ -207,6 +259,37 @@ function runStaticMethodStringCall(int $iterations): int 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 { $target = new DynamicCallTarget(); @@ -262,6 +345,57 @@ function runPolymorphicMethodReceiverCall(int $iterations): int 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 { return match ($case) { @@ -275,11 +409,20 @@ function runDynamicCallCase(string $case, int $iterations): int 'closure_monomorphic' => runMonomorphicClosureCall($iterations), 'closure_alternating' => runAlternatingClosureCall($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), 'invokable_object' => runInvokableObjectCall($iterations), 'method_name_monomorphic' => runMonomorphicMethodNameCall($iterations), 'method_name_alternating' => runAlternatingMethodNameCall($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}"), }; } @@ -319,11 +462,20 @@ function main(): void 'closure_monomorphic', 'closure_alternating', 'static_method_string', + 'static_class_dynamic', + 'static_method_dynamic', + 'static_class_method_dynamic', 'object_method_array', 'invokable_object', 'method_name_monomorphic', 'method_name_alternating', '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) { if (is_string($selectedCase) && $selectedCase !== '' && $selectedCase !== $case) { continue; diff --git a/benchmark/dynamic-call/run.php b/benchmark/dynamic-call/run.php index e6691f83..58901fff 100644 --- a/benchmark/dynamic-call/run.php +++ b/benchmark/dynamic-call/run.php @@ -86,6 +86,7 @@ if (!$skipBuild) { echo "Building TypePHP benchmark (-O3 + LTO)...\n"; runDynamicCallCommand([ $compilerPhp, + '-n', $root . '/bin/tpc.php', $project, '-j', @@ -135,11 +136,20 @@ $cases = [ 'closure_monomorphic', 'closure_alternating', 'static_method_string', + 'static_class_dynamic', + 'static_method_dynamic', + 'static_class_method_dynamic', 'object_method_array', 'invokable_object', 'method_name_monomorphic', 'method_name_alternating', '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 !== '') { $cases = [$selectedCase]; diff --git a/benchmark/static-cache/README.md b/benchmark/static-cache/README.md index f9890376..8662b1e4 100644 --- a/benchmark/static-cache/README.md +++ b/benchmark/static-cache/README.md @@ -1,9 +1,11 @@ # Static class cache benchmark -This benchmark covers a common metadata-cache pattern: a static array keyed by -`static::class`, guarded by `isset()`, plus a wrapper method using -`static::method()`. It measures static-property lookup, array lookup, strict -return checks, and late-static dispatch together. +This benchmark contains isolated reads and writes of statically resolved +`self::$property` and `Class::$property` slots. It also covers a common +metadata-cache pattern: a static array keyed by `static::class`, guarded by +`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: diff --git a/benchmark/static-cache/benchmark.php b/benchmark/static-cache/benchmark.php index bcd1fefa..fa3966b7 100644 --- a/benchmark/static-cache/benchmark.php +++ b/benchmark/static-cache/benchmark.php @@ -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 { $best = PHP_FLOAT_MAX; @@ -65,11 +127,21 @@ function main(): void { StaticCacheData::getData(); + [$explicitRead, $explicitReadChecksum] = measureExplicitStaticRead(); + StaticSlotData::$counter = 1; + [$selfRead, $selfReadChecksum] = StaticSlotData::measureSelfRead(); + [$selfWrite, $selfWriteChecksum] = StaticSlotData::measureSelfWrite(); [$getData, $getDataChecksum] = measureStaticCacheGetData(); [$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_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_table={$getTableChecksum}\n"; } diff --git a/benchmark/static-cache/run.php b/benchmark/static-cache/run.php index 4db01631..73942375 100644 --- a/benchmark/static-cache/run.php +++ b/benchmark/static-cache/run.php @@ -105,7 +105,7 @@ $typephpResult = parseStaticCacheResult(runStaticCacheCommand([$binary], $root, echo "Runtime: {$phpRuntime}\n"; echo "Metric PHP ns/op TypePHP ns/op TypePHP/PHP\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'; $checksum = 'checksum_' . $case; if (!isset($phpResult[$metric], $typephpResult[$metric])) { diff --git a/docs/en/SCOPE_MANAGEMENT.md b/docs/en/SCOPE_MANAGEMENT.md index 714add33..89923477 100644 --- a/docs/en/SCOPE_MANAGEMENT.md +++ b/docs/en/SCOPE_MANAGEMENT.md @@ -103,11 +103,11 @@ If a method never uses scoped dynamic calls, first-class callables, or scoped ca ### 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()` @@ -287,9 +287,9 @@ Likewise, `EG(fake_scope)` must not be unconditionally set at the entry of every ```text AOT method entry -> lazily generated CallableScope - -> php::callScoped() - -> CallableScope::resolve() - -> zend_is_callable_at_frame(synthetic frame) + -> typephp_call_method_scoped_cached() + -> call-site cache hit, or CallableScope::resolve() + -> zend_is_callable_at_frame(synthetic frame) on cache miss -> zend_call_function() ``` @@ -345,7 +345,7 @@ save EG(fake_scope) | Path | Main Cost | Optimization Strategy | | --- | --- | --- | | `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 | | `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 | diff --git a/docs/zh-cn/SCOPE_MANAGEMENT.md b/docs/zh-cn/SCOPE_MANAGEMENT.md index f378ec2c..10cbbb61 100644 --- a/docs/zh-cn/SCOPE_MANAGEMENT.md +++ b/docs/zh-cn/SCOPE_MANAGEMENT.md @@ -103,11 +103,11 @@ php::CallableScope tmp_var_1 = php::getCallableScope( ### 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()` @@ -287,9 +287,9 @@ fake_scope_guard.restore(); ```text AOT method entry -> lazily generated CallableScope - -> php::callScoped() - -> CallableScope::resolve() - -> zend_is_callable_at_frame(synthetic frame) + -> typephp_call_method_scoped_cached() + -> 调用点缓存命中,或 CallableScope::resolve() + -> 缓存未命中时执行 zend_is_callable_at_frame(synthetic frame) -> zend_call_function() ``` @@ -345,7 +345,7 @@ save EG(fake_scope) | 路径 | 主要成本 | 优化策略 | | --- | --- | --- | | `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 | | `makeScopedCallable()` | callable 解析及 Closure 分配 | 仅 first-class callable 使用 | | `UserCodeScopeGuard` | 方法入口一次指针查找、写入和退出恢复 | 只为 `call_user_func*`、callback map 或未解析的 unpack callback 生成 | diff --git a/phpunit/code/call-cache-sites.php b/phpunit/code/call-cache-sites.php index 54abcf9e..c09e8f98 100644 --- a/phpunit/code/call-cache-sites.php +++ b/phpunit/code/call-cache-sites.php @@ -5,9 +5,36 @@ function call_cache_target(int $value): int 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 diff --git a/phpunit/code/static-property-function-local-cache.php b/phpunit/code/static-property-function-local-cache.php new file mode 100644 index 00000000..e0135376 --- /dev/null +++ b/phpunit/code/static-property-function-local-cache.php @@ -0,0 +1,27 @@ +assertMatchesRegularExpression('/\.attr\([^)]+\)[^;]*--/', $cpp); // static-property postfix must NOT be rewritten - $this->assertMatchesRegularExpression('/typephp_get_static_property\([^)]+\)[^;]*\+\+/', $cpp); - $this->assertMatchesRegularExpression('/typephp_get_static_property\([^)]+\)[^;]*--/', $cpp); + $this->assertMatchesRegularExpression('/_typephp_static_property_\d+\(\)\+\+/', $cpp); + $this->assertMatchesRegularExpression('/_typephp_static_property_\d+\(\)--/', $cpp); // array-element postfix must NOT be rewritten $this->assertMatchesRegularExpression('/\.item\([^)]+\)[^;]*\+\+/', $cpp); diff --git a/phpunit/src/MagicCallCodegenTest.php b/phpunit/src/MagicCallCodegenTest.php index 6c112251..09ad0d06 100644 --- a/phpunit/src/MagicCallCodegenTest.php +++ b/phpunit/src/MagicCallCodegenTest.php @@ -13,11 +13,11 @@ final class MagicCallCodegenTest extends \BaseTest self::assertStringNotContainsString('.call(', $exactBody); $runtimeBody = $this->functionBody($code, 'php_runtimemagiccall'); - self::assertStringContainsString('.call(', $runtimeBody); + self::assertStringContainsString('typephp_call_method_cached(', $runtimeBody); self::assertStringNotContainsString('php_exactmagichandler____call(', $runtimeBody); $internalBody = $this->functionBody($code, 'php_exactinternalmethod'); - self::assertStringContainsString('.call(', $internalBody); + self::assertStringContainsString('typephp_call_method_cached(', $internalBody); self::assertStringNotContainsString('__call(', $internalBody); } diff --git a/phpunit/src/NativePropertyTest.php b/phpunit/src/NativePropertyTest.php index 76925685..d3a5c664 100644 --- a/phpunit/src/NativePropertyTest.php +++ b/phpunit/src/NativePropertyTest.php @@ -38,9 +38,15 @@ class NativePropertyTest extends \BaseTest } $code = file_get_contents($outputFile); - $this->assertStringContainsString('tmp_var_0 = typephp_get_called_class(this_);', $code); - $this->assertStringContainsString('php::getStaticProperty((tmp_var_0.isObject()', $code); - $this->assertStringContainsString('php::getStaticProperty((tmp_var_0.isObject() ? php::fn::get_class(tmp_var_0)', $code); + $this->assertStringContainsString( + 'zend_class_entry *const _typephp_called_ce = typephp_get_called_ce(this_);', + $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); } diff --git a/phpunit/src/StaticPropertyFunctionLocalCacheTest.php b/phpunit/src/StaticPropertyFunctionLocalCacheTest.php new file mode 100644 index 00000000..ab7d107a --- /dev/null +++ b/phpunit/src/StaticPropertyFunctionLocalCacheTest.php @@ -0,0 +1,54 @@ +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')); + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index a28abe74..ad87b290 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1327,6 +1327,21 @@ class CompilerBase implements PropertyAccessContext 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 { $id = $this->getClassId($className); @@ -3914,7 +3929,7 @@ class CompilerBase implements PropertyAccessContext if ($this->classDef?->nativeObject) { $this->fatalError($expr, 'Native classes do not support `new static()`'); } - $cePtr = Symbol::getCalledCe(); + $cePtr = $this->getCalledCeExpr(); } else { if ($className === 'self') { $className = $this->getFullClassName(); @@ -4112,7 +4127,7 @@ class CompilerBase implements PropertyAccessContext if (!$this->classDef) { $this->fatalError($class, 'Cannot use "static" outside a class'); } - return Symbol::getCalledCe(); + return $this->getCalledCeExpr(); } else { $className = $this->getNamespacedClassName($className); } @@ -4733,7 +4748,7 @@ class CompilerBase implements PropertyAccessContext if ($id === 'self') { $id = $this->getFullClassName(); } elseif ($id === 'static') { - return Symbol::getCalledClass(); + return $this->getCalledClassExpr(); } if ($this->isNameExpr($node) or $this->isIdExpr($node)) { 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_);' . 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); foreach ($this->context->classEntryPtrs as $className => $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; } } - foreach ($this->context->staticPropRefs as $name => $info) { - $getter = Symbol::getStaticProperty() . '(' . $info['classPtr'] . ', ' . $info['offsetExpr'] . ')'; - if (($info['kind'] ?? 'zval') === 'var') { - $code .= $this->getIndent() . Type::VAR . ' ' . $name . ' = ' . $getter . ';' . PHP_EOL; - } else { - $code .= $this->getIndent() . 'zval *' . $name . ' = ' . $getter . '.unwrap_ptr();' . PHP_EOL; - } + foreach ($this->context->staticPropRefs as $info) { + $code .= $this->getIndent() . 'zval *' . $info['name'] . ' = nullptr;' . PHP_EOL; + $code .= $this->getIndent() . 'const auto ' . $info['accessorName'] . ' = [&]() {' + . ' return typephp_get_static_property_cached(' . $info['name'] . ', [&]() {' + . ' return ' . $info['resolver'] . '; }); };' . PHP_EOL; } return $code; } diff --git a/src/Context/FunctionContext.php b/src/Context/FunctionContext.php index 1a90592c..90e8227c 100644 --- a/src/Context/FunctionContext.php +++ b/src/Context/FunctionContext.php @@ -83,6 +83,10 @@ class FunctionContext public array $classEntryPtrs = []; /** Reusable php::CallableScope local, created only when this function performs scoped calls. */ 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. */ public bool $needsUserCodeCallableScope = false; public int $tmpVarIndex = 0; @@ -112,7 +116,7 @@ class FunctionContext public array $beforeStmtLines = []; public array $afterStmtLines = []; 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 int $scopeLevel = 0; /** @@ -145,6 +149,8 @@ class FunctionContext $this->ceWrappers = []; $this->classEntryPtrs = []; $this->callableScopeVar = null; + $this->needsCalledCe = false; + $this->needsCalledClass = false; $this->tmpVarIndex = 0; $this->scopeLayouts = []; $this->callableScopeVar = null; @@ -188,6 +194,8 @@ class FunctionContext $this->hoistedProps = []; $this->staticPropRefs = []; $this->classEntryPtrs = []; + $this->needsCalledCe = false; + $this->needsCalledClass = false; $this->scopeLayouts = []; $this->scopeLevel = 0; $this->inLoop = false; diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index daf6e4a0..8492b555 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -31,7 +31,7 @@ trait ClosureGenerator // PHP flattens a trait method into the consuming class. A closure // declared in that method therefore uses the consuming class as // its lexical scope, never the trait's own class entry. - $scope = 'php::getCalledCe(this_)'; + $scope = $this->getCalledCeExpr(); } else { $scope = $this->class ? $this->getClassEntryPtr($this->getFullClassName()) diff --git a/src/Generator/TypeCheckGenerator.php b/src/Generator/TypeCheckGenerator.php index 60d0091a..a500461c 100644 --- a/src/Generator/TypeCheckGenerator.php +++ b/src/Generator/TypeCheckGenerator.php @@ -291,7 +291,7 @@ trait TypeCheckGenerator 'iterable' => '(' . $v . '.isArray() || (' . $v . '.isObject() && php::instanceOf(' . $v . ', zend_ce_traversable)))', 'allOf' => $this->genAllOfTypeCondition($varName, $entry['types']), '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']) . '))', default => '', }; diff --git a/src/Parser/ClassConstantFetchTrait.php b/src/Parser/ClassConstantFetchTrait.php index f262e1b3..3b0d0bb9 100644 --- a/src/Parser/ClassConstantFetchTrait.php +++ b/src/Parser/ClassConstantFetchTrait.php @@ -139,9 +139,9 @@ trait ClassConstantFetchTrait $this->fatalError($expr, "The 'static' keyword can only be used as the class name in class methods"); } if ($const === 'class') { - return Symbol::getCalledClass(); + return $this->getCalledClassExpr(); } 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) { $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_') { $ce = $this->getClassEntryPtr($this->getFullClassName()); } elseif ($class === 'parent') { diff --git a/src/Parser/ConstantExpressionTrait.php b/src/Parser/ConstantExpressionTrait.php index 42542098..86e6adfa 100644 --- a/src/Parser/ConstantExpressionTrait.php +++ b/src/Parser/ConstantExpressionTrait.php @@ -161,7 +161,7 @@ trait ConstantExpressionTrait $this->fatalError($expr, 'The magic constant `__CLASS__` is not allowed in global scope'); } if ($this->classDef->trait) { - return Symbol::getCalledClass(); + return $this->getCalledClassExpr(); } return '"' . $this->escapeString($class) . '"'; case 'Scalar_MagicConst_Trait': diff --git a/src/Parser/FunctionCallTrait.php b/src/Parser/FunctionCallTrait.php index 8e1fdb1b..ad1f2f6c 100644 --- a/src/Parser/FunctionCallTrait.php +++ b/src/Parser/FunctionCallTrait.php @@ -36,7 +36,8 @@ trait FunctionCallTrait } $callable = $this->parseExprAsValue($expr->right); - return 'php::call(' . $callable . ', {' . $value . '})'; + return 'typephp_call_cached(' . $callable . ', ' . $this->getFunctionCallCache() + . ', {' . $value . '})'; } /** diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 09bc18aa..b3bafc54 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -487,7 +487,7 @@ trait MethodCallTrait $staticCall = (bool) ($this->methodDef->flags & Modifiers::STATIC); } if ($staticCall) { - $callable = Symbol::getCalledCe() . ', ' . $methodPtr; + $callable = $this->getCalledCeExpr() . ', ' . $methodPtr; if (empty($expr->args)) { return 'php::call(' . $callable . ')'; } @@ -847,9 +847,11 @@ trait MethodCallTrait $magicMethod, $this->isVarExpr($expr->var) && $this->parseIdentifier($expr->var) === 'this_', ); + $resolvedMethodPtr = false; if ($class && $funcName && !$magicMethod) { if ($this->isInternalClass($class)) { $methodPtr = $this->getMethodPtr($class, $funcName); + $resolvedMethodPtr = true; } else { $methodPtr = $method; } @@ -859,9 +861,13 @@ trait MethodCallTrait if (empty($expr->args)) { 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() . ')'; } - if (!$this->isNamedMethod($expr->name)) { + if (!$resolvedMethodPtr) { return 'typephp_call_method_cached(' . $object . ', ' . $methodPtr . ', ' . $this->getMethodCallCache() . ')'; } @@ -869,9 +875,15 @@ trait MethodCallTrait } try { $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 . ', ' - . $this->getMethodCallCache() . ', ' . $this->parseCallArgs($expr->args) . ')'; + . $this->getMethodCallCache() . ', ' . $callArgs . ')'; } return $this->genRuntimeObjectMethodCall( $object, @@ -994,7 +1006,7 @@ trait MethodCallTrait return null; } - $calledCe = Symbol::getCalledCe(); + $calledCe = $this->getCalledCeExpr(); $direct = 'php::Var(' . self::PREFIX . $nativeFunc . '(this_))'; $fallback = 'php::call(' . $calledCe . ', php::getMethod(' . $calledCe . ', ' . $methodPtr . '))'; return '(EXPECTED(' . $calledCe . ' == ' . $this->getClassEntryPtr($class) . ')' @@ -1020,6 +1032,7 @@ trait MethodCallTrait $callScope = []; $rtFunc = ''; $rtClass = ''; + $cacheCallable = false; $canUseDirectCallScope = $this->isNameExpr($expr->class) && $this->isIdExpr($expr->name); $class = ($this->isNameExpr($expr->class) || $this->isVarExpr($expr->class)) ? $this->parseIdentifier($expr->class) @@ -1061,9 +1074,11 @@ trait MethodCallTrait } } $placeHolder = $fn; + $cacheCallable = true; } elseif ($this->isVarExpr($expr->name)) { $fn = 'php::concat({' . $this->identifierToStr($expr->class) . ', "::", ' . $this->methodNameToStr($expr->name) . '})'; $placeHolder = $fn; + $cacheCallable = true; } elseif ($class === 'static') { if ($this->classDef?->nativeObject) { $this->fatalError( @@ -1077,14 +1092,15 @@ trait MethodCallTrait if ($exactCall !== null) { return $exactCall; } - $fn = Symbol::getCalledCe() . ', php::getMethod(' . Symbol::getCalledCe() . ', ' . $methodPtr . ')'; + $calledCe = $this->getCalledCeExpr(); + $fn = $calledCe . ', php::getMethod(' . $calledCe . ', ' . $methodPtr . ')'; if ($this->debug) { $this->context->beforeStmtLines[] = $this->formatCppLineComment( 'Static Method Call: ', '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) $rtFunc = $method; $rtClass = $this->getFullClassName(); @@ -1150,13 +1166,20 @@ trait MethodCallTrait // reusable handlers and never stores transient trampolines. $fn = $this->getLiteralString($class . '::' . $method); $placeHolder = $this->genArray($callScope); + $cacheCallable = true; } - $call = 'php::call'; if (empty($expr->args)) { - return $call . '(' . $fn . ')'; + if ($cacheCallable) { + return 'typephp_call_cached(' . $fn . ', ' . $this->getFunctionCallCache() . ')'; + } + return 'php::call(' . $fn . ')'; } try { + if ($cacheCallable) { + return 'typephp_call_cached(' . $fn . ', ' . $this->getFunctionCallCache() . ', ' + . $this->parseCallArgs($expr->args, $rtFunc, $rtClass) . ')'; + } return $this->genRuntimeFunctionCall($fn, $expr->args, $rtFunc, $rtClass); } catch (PlaceHolder) { return $this->genPlaceHolder($placeHolder); diff --git a/src/Parser/NullsafeAccessTrait.php b/src/Parser/NullsafeAccessTrait.php index 194f7424..24e4d810 100644 --- a/src/Parser/NullsafeAccessTrait.php +++ b/src/Parser/NullsafeAccessTrait.php @@ -119,10 +119,12 @@ trait NullsafeAccessTrait } if ($requiresDynamicScope && $this->methodDef) { $code .= $this->getIndent() - . "{$tmpVar} = php::callScoped({$object}, {$item[1]}, " - . $this->getCallableScopeExpr() . ", {$args});" . PHP_EOL; + . "{$tmpVar} = typephp_call_method_scoped_cached({$object}, {$item[1]}, " + . $this->getCallableScopeExpr() . ', ' . $this->getMethodCallCache() + . ", {$args});" . PHP_EOL; } 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) { $code .= $this->formatCapturedStmtLines($argAfterStmts); diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index c0af3569..e7f338cf 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -419,7 +419,7 @@ trait PropertyAccessTrait } if ($resolution->expression !== null) { // 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 ($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, $this->getFullClassName(), null); @@ -480,6 +480,29 @@ trait PropertyAccessTrait 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); if ($resolution !== null) { $nativeProp = $resolution->expression; @@ -492,8 +515,25 @@ trait PropertyAccessTrait if ($resolution->nativeProperty && $class !== null) { $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); - return Symbol::getResolvedStaticProperty() . '(' . $classPtr . ', ' . $nativeProp . ')'; + return $slot; } else { $this->setNativePropertyValueSource($expr, self::NATIVE_PROPERTY_VALUE_DYNAMIC); return $nativeProp; @@ -510,29 +550,39 @@ trait PropertyAccessTrait ): string { $info = $this->getHoistedObjectPropInfo($def->type); $propName = $this->parseIdentifier($expr->name); - $refVar = '_static_' . str_replace('\\', '_', $class) . '_' . $propName; - $this->registerStaticPropertyRef($refVar, $class, $nativeProp, $info); + $classPtr = $this->getClassEntryPtr($class); + $slot = $this->registerStaticPropertySlot( + $class . '::$' . $propName, + 'typephp_get_static_property_slot(' . $classPtr . ', ' . $nativeProp . ')', + ); if ($info['kind'] === 'zval') { $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])) { - return; + if (isset($this->context->staticPropRefs[$key])) { + return $this->context->staticPropRefs[$key]['accessorName'] . '()'; } - $this->context->staticPropRefs[$refVar] = [ - 'type' => $info['type'], - 'classPtr' => $this->getClassEntryPtr($class), - 'offsetExpr' => $offsetExpr, - 'kind' => $info['kind'], + $name = '_typephp_static_property_slot_' . count($this->context->staticPropRefs); + $accessorName = '_typephp_static_property_' . count($this->context->staticPropRefs); + $this->context->staticPropRefs[$key] = [ + 'name' => $name, + 'accessorName' => $accessorName, + 'resolver' => $resolver, ]; + return $accessorName . '()'; } protected function parseStaticPropertyFetch(Expr\StaticPropertyFetch $expr): string @@ -598,7 +648,7 @@ trait PropertyAccessTrait if (!$this->methodDef) { $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)); diff --git a/tests/compiler/dynamic_call/call-cache-dispatch.phpt b/tests/compiler/dynamic_call/call-cache-dispatch.phpt index 2141b8a3..59c67c6f 100644 --- a/tests/compiler/dynamic_call/call-cache-dispatch.phpt +++ b/tests/compiler/dynamic_call/call-cache-dispatch.phpt @@ -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 { return $callback($value); @@ -55,6 +84,36 @@ function invoke_method(object $object, mixed $method, int $value): string 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 { $callbacks = ['cached_first', 'cached_second', 'cached_first']; @@ -72,6 +131,24 @@ function main(): void foreach ($objects as $index => $object) { 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-- @@ -83,3 +160,16 @@ string(9) "closure:4" string(14) "method-first:5" string(15) "method-second:6" 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" diff --git a/tests/compiler/static/static-property-function-local-slot.phpt b/tests/compiler/static/static-property-function-local-slot.phpt new file mode 100644 index 00000000..dfd73bc2 --- /dev/null +++ b/tests/compiler/static/static-property-function-local-slot.phpt @@ -0,0 +1,89 @@ +--TEST-- +Static-property slot caches retain live values, references, inheritance and late static binding +--FILE-- + +--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"