feat(scope): enhance callback scope management with dynamic callable support

- Add normalizeCallableClass helper for dynamic callback normalization
- Introduce FunctionContext::needsUserCodeCallableScope for user-code scope tracking
- Replace MethodDef::needsUnpackedCallbackScope with per-context scope management
- Update callback descriptors to include handling strategies (callable, dynamic, map)
- Implement proper callable scope guard generation for closures and fibers
- Add propertyNameToStr method for correct property name conversion
- Refactor scope guard generation to use CallableScope with lexical scope
- Update documentation for enhanced scope management system
- Add tests for complex scoped callback scenarios
- Fix parent class name normalization in property access resolver
- Remove deprecated unpacked callback scope fallback mechanism
pull/48/head
韩天峰 2 weeks ago
parent 8a3b5ae9a4
commit e07b97d6cf
  1. 43
      docs/SCOPE_MANAGEMENT.md
  2. 4
      phpunit/src/Context/FunctionContextTest.php
  3. 13
      phpunit/src/Entity/MethodDefExtendedTest.php
  4. 1
      phpunit/src/Entity/MethodDefTest.php
  5. 44
      phpunit/src/Resolver/PropertyAccessResolverTest.php
  6. 10
      phpunit/src/TraitsTest.php
  7. 65
      src/CompilerBase.php
  8. 2
      src/Context/FunctionContext.php
  9. 3
      src/Entity/MethodDef.php
  10. 10
      src/Generator/CallArgumentGenerator.php
  11. 13
      src/Generator/ClosureGenerator.php
  12. 6
      src/Generator/FiberGenerator.php
  13. 2
      src/Optimizer/FuncCallOptimizer.php
  14. 4
      src/Parser/NullsafeAccessTrait.php
  15. 22
      src/Parser/PropertyAccessTrait.php
  16. 2
      src/Python/PythonModuleTrait.php
  17. 5
      src/Resolver/PropertyAccessResolver.php
  18. 4
      src/Translator.php
  19. 128
      tests/compiler/callable/scoped-callback-complex.phpt
  20. 28
      tests/compiler/object_property/property-named-static.phpt

@ -24,13 +24,13 @@ Scope 设计遵循以下原则:
| 管理器 | 管理的状态 | 主要用途 | 是否修改 Zend 当前状态 |
| --- | --- | --- | --- |
| `php::CallableScope` | synthetic `zend_execute_data`,包含 lexical scope、called scope 和 `$this` | 动态方法调用、first-class callable、内置函数 callback | 否 |
| `php::UserCodeScopeGuard` | 最近 user-code frame 的 `zend_function::common.scope` | callback 隐藏在参数展开中的兜底路径 | 是,析构时恢复 |
| `php::UserCodeScopeGuard` | 最近 user-code frame 的 `zend_function::common.scope` | `call_user_func*` 及 callback 隐藏在参数展开中的动态调用路径 | 是,析构时恢复 |
| `php::FakeScopeGuard` | `EG(fake_scope)` | Zend 属性、对象、异常等读取 fake scope 的 API | 是,析构或显式 `restore()` 时恢复 |
选择规则可以简化为:
- 能拿到明确 callable 值:使用 `CallableScope`
- callback 藏在 `...$args`,编译期无法知道最终参数位置:使用 `UserCodeScopeGuard`
- 调用 `call_user_func*`,或其他内置函数的 callback 藏在 `...$args` 中:使用 `UserCodeScopeGuard`
- 调用的 Zend API 明确读取 `EG(fake_scope)`:使用 `FakeScopeGuard`
- 纯 native 调用或不依赖调用者可见性的操作:不创建任何 Scope 管理器。
@ -153,7 +153,7 @@ $callback = [$class, 'method'];
### 4.1 职责与适用范围
`UserCodeScopeGuard` 只处理一种编译器无法静态改写 callback 的情况:callback 位于参数展开中
`UserCodeScopeGuard` 服务于完全动态的 `call_user_func()` / `call_user_func_array()`,以及编译器无法静态改写 callback 的参数展开场景
```php
$args = [[$this, 'privateMethod'], 1];
@ -162,7 +162,15 @@ call_user_func(...$args);
内置函数 callback 可能位于固定位置、倒数位置、命名参数中,甚至一个函数有多个 callback。执行 `...$args` 展开前,编译器并不知道最终的 positional/named 参数布局,无法只对对应值调用 `prepareScopedCallback()`
普通 callback 参数不得使用此 guard;只要 callback 的 AST 参数位置已知,就应使用 `CallableScope` 路径。
`call_user_func*` 本身就是 ZendVM 的完全动态调用边界,无论 callback 是否显式出现,都不创建 fake Closure。如果 callable 数组使用 `self`、`parent` 或 `static`,则先由 `normalizeCallableClass()` 将 class 部分转换为真实类名:
- `self` 转为 `CallableScope::lexicalScope()`
- `parent` 转为 lexical scope 的父类;
- `static` 转为 `CallableScope::calledScope()`
规范化只复制需要修改的 callback 数组。绝对类名、对象 callback、Closure 和普通函数名保持原值。
`call_user_func*` 外,普通 callback 参数不得使用此 guard;只要 callback 的 AST 参数位置已知,就应使用 `CallableScope` 路径。
### 4.2 实现方式
@ -174,18 +182,18 @@ while (frame && (!frame->func || !ZEND_USER_CODE(frame->func->type))) {
}
```
找到后保存:
找到后保存,并使用 `CallableScope::lexicalScope()` 设置可见性作用域
```cpp
function_ = frame->func;
previous_scope_ = function_->common.scope;
function_->common.scope = requested_scope;
function_->common.scope = callable_scope.lexicalScope();
```
析构函数恢复 `previous_scope_`。类不可复制、不可移动,保证一次构造对应一次恢复。如果没有可用的 user-code frame,会抛出:
```text
A user-code frame is required for scoped callback argument unpacking
A user-code frame is required for scoped dynamic callback calls
```
该 guard 操作的是从当前请求执行链找到的 user-code frame,不是 TypePHP 注册在 MINIT 的 persistent internal method。`EG(current_execute_data)` 本身属于当前 executor 上下文。其影响窗口被限制在当前 AOT 方法调用的 RAII 生命周期内。
@ -195,16 +203,21 @@ A user-code frame is required for scoped callback argument unpacking
编译器维护语义明确的标记:
```php
MethodDef::$needsUnpackedCallbackScope
FunctionContext::$needsUserCodeCallableScope
```
一个已知会同步调用 callback 的 PHP 内置函数存在参数展开,且 callback 无法从显式参数中完整匹配时,`markUnpackedCallbackScopeFallback()` 设置该标记。方法、Closure 或 Fiber 入口只生成一个:
编译器遇到 `call_user_func*` 的动态 callback,或一个已知会同步调用 callback 的内置函数存在无法匹配的参数展开时,`markUserCodeCallableScope()` 设置该标记。状态属于当前 `FunctionContext`,因此普通方法、嵌套 Closure 和 Fiber 各自独立,不会把 guard 错误泄漏到外层函数。每个函数体入口只生成一个:
```cpp
php::UserCodeScopeGuard tmp_var_2{php_get_called_ce(this_)};
php::CallableScope tmp_var_1 = php_get_callable_scope(..., this_);
php::UserCodeScopeGuard tmp_var_2{tmp_var_1};
```
它不是按 call site 或循环迭代创建的。没有 unpack callback 的方法不会产生此成本。
即使调用形态是 `call_user_func($closure)`,且 Closure 内部再次通过
`call_user_func(['self', 'method'])` 调用,每一层也只读取自己的
`FunctionContext`、lexical scope 和 `$this`,不能复用或污染外层 guard。
它不是按 call site 或循环迭代创建的。没有上述动态 callback 的方法不会产生此成本。
### 4.4 为什么当前保留该兜底
@ -340,7 +353,7 @@ save EG(fake_scope)
| `prepareScopedCallback()` | 一次 callable 解析 | public 绝对 callback 不创建 Closure |
| `makeScopedCallable()` | callable 解析及 Closure 分配 | 仅 first-class callable 使用 |
| `makeScopedCallableMap()` | O(N) 检查 | COW;只包装需要作用域的元素 |
| `UserCodeScopeGuard` | 方法入口一次指针查找、写入和退出恢复 | 只为未解析的 unpack callback 生成 |
| `UserCodeScopeGuard` | 方法入口一次指针查找、写入和退出恢复 | 只为 `call_user_func*`未解析的 unpack callback 生成 |
| `FakeScopeGuard` | 两次 executor-global 指针赋值 | 仅包围确实读取 fake scope 的 Zend API |
这套设计刻意让常见的纯 Native Call、无 callback 方法和 public callback 保持最短路径。不要为了统一表面形式而把低频 fallback 下沉到所有调用中。
@ -381,11 +394,13 @@ Scope 修改至少应覆盖以下层次:
| callback 参数包装 | `src/Generator/CallArgumentGenerator.php` |
| Closure/Fiber fallback guard | `src/Generator/ClosureGenerator.php`、`FiberGenerator.php` |
| 方法 fallback guard | `src/Translator.php` |
| Scope 状态 | `src/Context/FunctionContext.php`、`src/Entity/MethodDef.php` |
| Scope 状态 | `src/Context/FunctionContext.php` |
| 属性访问中的 fake scope | `src/Parser/PropertyAccessTrait.php` |
## 11. 后续演进原则
只有当编译器具备统一的“参数展开后绑定”中间表示,并能完整处理 positional、named、negative-position 和 callback map 时,才考虑删除 `UserCodeScopeGuard`。删除它的目标应是让所有 callback 都走显式 `CallableScope`,而不是重新扩大 `EG(fake_scope)` 或真实 frame 修改的范围。
`UserCodeScopeGuard` 是复杂动态调用的长期保留机制,不以删除为目标。它修改的是当前线程、当前请求中的 user-code frame,并通过 RAII 恢复;ZTS 下不同线程拥有各自的执行上下文,因此不会共享被修改的 frame 状态。
`CallableScope` 用于编译器能够确定 callback 位置与调用边界的单一场景,以减少 frame 修改和 Closure 包装;它是一条更快、更明确的路径,而不是要求覆盖 unpack、多层动态 callback 等所有场景。遇到难以静态证明安全的组合时,应优先保留 `UserCodeScopeGuard`,不要为了形式上的统一强行改写为 `CallableScope`
未来新增 Scope 抽象前,应先确认 Zend API 依赖的是 synthetic call frame、真实 user-code frame,还是 `EG(fake_scope)`。名称和类型应直接表达所管理的 Zend 状态,避免再次出现一个含义过宽的通用 `Scope` 类。

@ -29,6 +29,7 @@ class FunctionContextTest extends TestCase
$this->assertSame(0, $ctx->scopeLevel);
$this->assertFalse($ctx->inLoop);
$this->assertFalse($ctx->inClosure);
$this->assertFalse($ctx->needsUserCodeCallableScope);
}
public function testEnterScopeIncrementsLevel(): void
@ -73,6 +74,9 @@ class FunctionContextTest extends TestCase
$ctx->tmpVarIndex = 5;
$this->assertSame(5, $ctx->tmpVarIndex);
$ctx->needsUserCodeCallableScope = true;
$this->assertTrue($ctx->needsUserCodeCallableScope);
}
public function testLocalVarsManipulation(): void

@ -10,19 +10,6 @@ use PhpParser\Modifiers;
class MethodDefExtendedTest extends TestCase
{
public function testUnpackedCallbackScopeDefaultsToFalse(): void
{
$method = new MethodDef(Modifiers::PUBLIC, 'test');
$this->assertFalse($method->needsUnpackedCallbackScope);
}
public function testUnpackedCallbackScopeCanBeSet(): void
{
$method = new MethodDef(Modifiers::PUBLIC, 'test');
$method->needsUnpackedCallbackScope = true;
$this->assertTrue($method->needsUnpackedCallbackScope);
}
public function testStaticMethod(): void
{
$method = new MethodDef(Modifiers::PUBLIC | Modifiers::STATIC, 'factory');

@ -16,7 +16,6 @@ class MethodDefTest extends TestCase
$this->assertEquals('handle', $method->name);
$this->assertSame(Modifiers::PUBLIC, $method->flags);
$this->assertNull($method->functionDef);
$this->assertFalse($method->needsUnpackedCallbackScope);
}
public function testGetReturnType(): void

@ -0,0 +1,44 @@
<?php
namespace TypePhp\Tests\Resolver;
use PhpParser\NodeAbstract;
use PHPUnit\Framework\TestCase;
use TypePhp\Entity\ClassDef;
use TypePhp\Resolver\PropertyAccessContext;
use TypePhp\Resolver\PropertyAccessResolver;
final class PropertyAccessResolverTest extends TestCase
{
public function testSubclassLookupNormalizesEveryParentName(): void
{
$parents = [
'app\\childexception' => 'RuntimeException',
];
$context = new class($parents) implements PropertyAccessContext {
public function __construct(private array $parents)
{
}
public function getClassDef(string $name): ?ClassDef
{
return null;
}
public function getParentClass(string $class): string
{
return $this->parents[strtolower(ltrim($class, '\\'))] ?? '';
}
public function fatalError(NodeAbstract $node, string $msg): never
{
throw new \LogicException($msg);
}
};
$resolver = new PropertyAccessResolver($context);
$this->assertTrue($resolver->isSameOrSubclassOf('App\\ChildException', 'runtimeexception'));
$this->assertTrue($resolver->canAccessProtectedProperty('App\\ChildException', 'RuntimeException'));
}
}

@ -215,16 +215,16 @@ class TraitsTest extends TestCase
}
// ========================================================================
// ClosureGenerator::genUnpackedCallbackScopeGuard
// ClosureGenerator::genUserCodeCallableScopeGuard
// ========================================================================
public function testGenUnpackedCallbackScopeGuard(): void
{
$this->invoke('resetFunction');
$result = $this->invoke('genUnpackedCallbackScopeGuard');
$result = $this->invoke('genUserCodeCallableScopeGuard');
$this->assertStringContainsString('php::UserCodeScopeGuard', $result);
$this->assertStringContainsString('php_get_called_ce(this_)', $result);
$this->assertStringContainsString('php::CallableScope(nullptr, nullptr, nullptr)', $result);
$this->assertStringNotContainsString('ON_SCOPE_EXIT', $result);
}
@ -232,9 +232,9 @@ class TraitsTest extends TestCase
{
$this->invoke('resetFunction');
$result = $this->invoke('genUnpackedCallbackScopeGuard');
$result = $this->invoke('genUserCodeCallableScopeGuard');
$this->assertMatchesRegularExpression(
'/php::UserCodeScopeGuard tmp_var_\d+\{php_get_called_ce\(this_\)\};/',
'/php::UserCodeScopeGuard tmp_var_\d+\{php::CallableScope\(nullptr, nullptr, nullptr\)\};/',
$result,
);
}

@ -3186,11 +3186,17 @@ class CompilerBase implements PropertyAccessContext
return $object . '.call(' . $method . ', ' . $callArgs . ')';
}
/** Retain the legacy frame scope when an unpacked value may contain a callback. */
protected function markUnpackedCallbackScopeFallback(): void
/** Reserve one reusable Zend user-code frame for scoped dynamic callbacks. */
protected function markUserCodeCallableScope(): void
{
if ($this->methodDef) {
$this->methodDef->needsUnpackedCallbackScope = true;
// This state belongs to the generated function body, not to its
// MethodDef. A method and each nested Closure have independent
// FunctionContext instances and therefore independent guards.
$this->context->needsUserCodeCallableScope = true;
// Reserve the declaration before the generated function prologue
// is emitted. The guard itself is inserted after parsing the body.
$this->getCallableScopeExpr();
}
}
@ -3211,9 +3217,9 @@ class CompilerBase implements PropertyAccessContext
* compiled by TypePHP itself, so the result must not depend on how a
* particular PHP build exposes callable parameter types.
*
* Each entry identifies callback arguments by their positional index
* and PHP named-argument name. Negative indexes count from the end.
* The boolean flag marks containers whose values are callbacks.
* Each entry identifies callback arguments by their positional index,
* PHP named-argument name and optional handling strategy. Negative
* indexes count from the end. The default strategy is `callable`.
*/
static $callbackArgs = [
'array_map' => [[0, 'callback']],
@ -3228,12 +3234,12 @@ class CompilerBase implements PropertyAccessContext
'usort' => [[1, 'callback']],
'uasort' => [[1, 'callback']],
'uksort' => [[1, 'callback']],
'call_user_func' => [[0, 'callback']],
'call_user_func_array' => [[0, 'callback']],
'call_user_func' => [[0, 'callback', 'dynamic']],
'call_user_func_array' => [[0, 'callback', 'dynamic']],
'forward_static_call' => [[0, 'callback']],
'forward_static_call_array' => [[0, 'callback']],
'preg_replace_callback' => [[1, 'callback']],
'preg_replace_callback_array' => [[0, 'pattern', true]],
'preg_replace_callback_array' => [[0, 'pattern', 'map']],
'iterator_apply' => [[1, 'callback']],
'array_udiff' => [[-1, 'value_compare_func']],
'array_udiff_assoc' => [[-1, 'value_compare_func']],
@ -3258,18 +3264,26 @@ class CompilerBase implements PropertyAccessContext
return;
}
$fullyDynamic = ($descriptors[0][2] ?? 'callable') === 'dynamic';
if ($fullyDynamic) {
$this->markUserCodeCallableScope();
}
$argCount = count($args);
$matchedCallbacks = [];
$hasUnpackedArg = false;
foreach ($args as $index => $arg) {
if ($arg->unpack) {
$hasUnpackedArg = true;
if ($fullyDynamic) {
$arg->setAttribute(self::ATTR_SCOPED_CALLBACK, 'normalize-unpacked');
}
continue;
}
foreach ($descriptors as $descriptorIndex => $descriptor) {
[$position, $name] = $descriptor;
$container = $descriptor[2] ?? false;
$strategy = $descriptor[2] ?? 'callable';
$container = $strategy === 'map';
$callbackPosition = $position < 0 ? $argCount + $position : $position;
$matches = $arg->name === null
? $index === $callbackPosition
@ -3277,7 +3291,8 @@ class CompilerBase implements PropertyAccessContext
if ($matches) {
$matchedCallbacks[$descriptorIndex] = true;
if ($container || !$this->isScopeIndependentCallableExpr($arg->value)) {
$arg->setAttribute(self::ATTR_SCOPED_CALLBACK, $container ? 'map' : 'callable');
$mode = $strategy === 'dynamic' ? 'normalize' : $strategy;
$arg->setAttribute(self::ATTR_SCOPED_CALLBACK, $mode);
}
// Some functions accept more than one callback (for
// example array_udiff_uassoc()). Mark every matching
@ -3287,12 +3302,12 @@ class CompilerBase implements PropertyAccessContext
}
}
if ($hasUnpackedArg && count($matchedCallbacks) !== count($descriptors)) {
if (!$fullyDynamic && $hasUnpackedArg && count($matchedCallbacks) !== count($descriptors)) {
// If the unpacked value itself supplies a callback, its runtime
// position is not known here. Keep the legacy frame scope only
// for this remaining case; ordinary callback arguments no longer
// mutate the executing Zend frame.
$this->markUnpackedCallbackScopeFallback();
// position is not known here. Retain a reusable user-code scope
// guard for this complex case; ordinary callback arguments use
// an explicit CallableScope instead.
$this->markUserCodeCallableScope();
}
}
@ -3422,7 +3437,7 @@ class CompilerBase implements PropertyAccessContext
}
$class = $this->identifierToStr($expr->var->class);
$prop = $this->identifierToStr($expr->var->name);
$prop = $this->propertyNameToStr($expr->var->name);
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, Type::VAR);
$this->context->beforeStmtLines[] = $tmpVar . ' = ' . Symbol::getStaticProperty() . '(' . $class . ', ' . $prop . ');';
@ -3869,7 +3884,7 @@ class CompilerBase implements PropertyAccessContext
$dim = $this->parseIdentifier($expr->dim);
$list[] = '{php::ArrayDimFetch, ' . Type::VAR . '(' . $dim . ')}';
} elseif ($this->isPropertyFetch($expr)) {
$name = $this->identifierToStr($expr->name, literal: true);
$name = $this->propertyNameToStr($expr->name, literal: true);
$list[] = '{php::PropertyFetch, ' . Type::VAR . '(' . $name . ')}';
} elseif ($this->isVarExpr($expr)) {
$var = $this->parseIdentifier($expr);
@ -3981,6 +3996,20 @@ class CompilerBase implements PropertyAccessContext
return $id;
}
/**
* Convert an object/static property name without interpreting `self` or
* `static` as a class-name keyword. Those words are valid PHP property
* names and only have special meaning in class-name positions.
*/
protected function propertyNameToStr(NodeAbstract $node, bool $require = true, bool $literal = false): string
{
if ($this->isIdExpr($node)) {
$name = $this->parseIdentifier($node);
return $literal ? $this->getLiteralString($name) : $this->genCharPtr($name, true);
}
return $this->identifierToStr($node, $require, $literal);
}
protected function requireVar($node, string $var): void
{
if (!$this->hasVar($var)) {

@ -55,6 +55,8 @@ class FunctionContext
public array $ceWrappers = [];
/** Reusable php::CallableScope local, created only when this function performs scoped calls. */
public ?string $callableScopeVar = null;
/** This generated body needs a temporary lexical scope on the nearest user-code frame. */
public bool $needsUserCodeCallableScope = false;
public int $tmpVarIndex = 0;
public array $arguments = [];
/** True while parsing a breakable loop or switch. */

@ -13,9 +13,6 @@ class MethodDef
public int $flags;
public string $name;
public ?FunctionDef $functionDef = null;
/** An unpacked argument may supply a callback whose lexical scope is only known at runtime. */
public bool $needsUnpackedCallbackScope = false;
/**
* The original `ClassMethod` AST node this definition was parsed from.
* Stored so that later validation (e.g. trait method override compatibility

@ -399,6 +399,10 @@ trait CallArgumentGenerator
if (!$forceArrayArgs && $separateNamedArgs) {
$callArgs = $this->ensureCallArgs($argsVar, $list_args);
$this->context->beforeStmtLines[] = $callArgs . '.appendUnpacked(' . $this->parseArrayArg($arg) . ');';
if ($arg->getAttribute(self::ATTR_SCOPED_CALLBACK) === 'normalize-unpacked') {
$this->context->beforeStmtLines[] = 'php::normalizeCallableClass('
. $callArgs . ', 0, ' . $this->getCallableScopeExpr() . ');';
}
} else {
$arrayArgs = $this->ensureCallArrayArgs($arrayArgsVar, $list_args);
$this->context->beforeStmtLines[] = $arrayArgs . '.merge(' . $this->parseArrayArg($arg) . ');';
@ -553,7 +557,11 @@ trait CallArgumentGenerator
return $value;
}
$helper = $mode === 'map' ? 'makeScopedCallableMap' : 'prepareScopedCallback';
$helper = match ($mode) {
'map' => 'makeScopedCallableMap',
'normalize' => 'normalizeCallableClass',
default => 'prepareScopedCallback',
};
return 'php::' . $helper . '(' . $value . ', ' . $this->getCallableScopeExpr() . ')';
}

@ -91,10 +91,10 @@ trait ClosureGenerator
return $stmts[array_key_last($stmts)] instanceof Node\Stmt\Return_;
}
protected function genUnpackedCallbackScopeGuard(): string
protected function genUserCodeCallableScopeGuard(): string
{
$tmpScope = $this->genTmpVarName();
return "php::UserCodeScopeGuard {$tmpScope}{php_get_called_ce(this_)};" . PHP_EOL;
return 'php::UserCodeScopeGuard ' . $tmpScope . '{' . $this->getCallableScopeExpr() . '};' . PHP_EOL;
}
protected function genClosure(Expr\ArrowFunction|Expr\Closure $expr, array $params, array $uses = []): string
@ -203,6 +203,9 @@ trait ClosureGenerator
$body = $isGenerator
? $this->genGeneratorClosureFactoryBody($expr, $params, $uses)
: $this->genClosureBody($expr);
if ($this->context->needsUserCodeCallableScope) {
$body = $this->genUserCodeCallableScopeGuard() . $body;
}
$code .= $this->genScopeVarDecl() . $body;
$this->indentLevel--;
@ -319,9 +322,6 @@ trait ClosureGenerator
$this->indentLevel++;
$body = '';
if ($this->methodDef && $this->methodDef->needsUnpackedCallbackScope) {
$body .= $this->genUnpackedCallbackScopeGuard();
}
if ($expr instanceof Expr\ArrowFunction) {
[$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr->expr);
$body .= $this->formatCapturedStmtLines($beforeStmts);
@ -338,6 +338,9 @@ trait ClosureGenerator
$body .= $this->getIndent() . 'return php::null;' . PHP_EOL;
}
}
if ($this->context->needsUserCodeCallableScope) {
$body = $this->genUserCodeCallableScopeGuard() . $body;
}
$this->indentLevel--;
$code .= $this->genScopeVarDecl();

@ -283,13 +283,13 @@ trait FiberGenerator
$body = '';
$this->indentLevel++;
if ($this->methodDef && $this->methodDef->needsUnpackedCallbackScope) {
$body .= $this->genUnpackedCallbackScopeGuard();
}
if ($v->stmts) {
$body .= $this->parseStmts($v->stmts);
}
$body .= $this->getIndent() . 'return ' . self::VALUE_NULL . ';' . PHP_EOL;
if ($this->context->needsUserCodeCallableScope) {
$body = $this->genUserCodeCallableScopeGuard() . $body;
}
$this->indentLevel--;
$code .= $this->genScopeVarDecl();
$code .= $this->getIndent() . 'try {' . PHP_EOL;

@ -371,7 +371,7 @@ trait FuncCallOptimizer
if ($this->isPropertyFetch($arg) and $this->isVarExpr($arg->var)) {
$obj = $this->parseIdentifier($arg->var);
$tmpRef = $this->genTmpVarName();
$this->context->beforeStmtLines[] = 'auto&& ' . $tmpRef . ' = ' . $obj . '.attr(' . $this->identifierToStr($arg->name) . ', php::AttrMode::Update);';
$this->context->beforeStmtLines[] = 'auto&& ' . $tmpRef . ' = ' . $obj . '.attr(' . $this->propertyNameToStr($arg->name) . ', php::AttrMode::Update);';
return $tmpRef;
}
return $this->getArg($expr, $i);

@ -43,13 +43,13 @@ trait NullsafeAccessTrait
while (1) {
if ($expr instanceof Expr\NullsafePropertyFetch) {
$list[] = ['property', $this->identifierToStr($expr->name, literal: true), $expr, true];
$list[] = ['property', $this->propertyNameToStr($expr->name, literal: true), $expr, true];
$expr = $expr->var;
} elseif ($expr instanceof Expr\NullsafeMethodCall) {
$list[] = ['method', $this->identifierToStr($expr->name, literal: true), $expr->args, true, $expr];
$expr = $expr->var;
} elseif ($expr instanceof Expr\PropertyFetch) {
$list[] = ['property', $this->identifierToStr($expr->name, literal: true), $expr, false];
$list[] = ['property', $this->propertyNameToStr($expr->name, literal: true), $expr, false];
$expr = $expr->var;
} elseif ($expr instanceof Expr\MethodCall) {
$list[] = ['method', $this->identifierToStr($expr->name, literal: true), $expr->args, false, $expr];

@ -110,7 +110,7 @@ trait PropertyAccessTrait
return $this->emitDynamicPropertyRead(
$this->parseIdentifier($expr->var),
$this->identifierToStr($expr->name, literal: true)
$this->propertyNameToStr($expr->name, literal: true)
);
}
@ -122,7 +122,7 @@ trait PropertyAccessTrait
return $this->emitDynamicPropertyWrite(
$this->parseIdentifier($expr->var),
$this->identifierToStr($expr->name, literal: true),
$this->propertyNameToStr($expr->name, literal: true),
$value
);
}
@ -142,7 +142,7 @@ trait PropertyAccessTrait
return $this->emitDynamicPropertyTargetUnset($target);
}
return $this->parseIdentifier($expr->var) . '.unsetProperty(' . $this->identifierToStr($expr->name, literal: true) . ')';
return $this->parseIdentifier($expr->var) . '.unsetProperty(' . $this->propertyNameToStr($expr->name, literal: true) . ')';
}
protected function emitDynamicPropertyFetchAppendArray(Expr\PropertyFetch $expr, string $value, ?PropertyWriteTarget $target = null): string
@ -156,7 +156,7 @@ trait PropertyAccessTrait
return $this->emitDynamicPropertyAppendArray(
$this->parseIdentifier($expr->var),
$this->identifierToStr($expr->name, literal: true),
$this->propertyNameToStr($expr->name, literal: true),
$value
);
}
@ -172,7 +172,7 @@ trait PropertyAccessTrait
return $this->emitDynamicPropertyUpdateArray(
$this->parseIdentifier($expr->var),
$this->identifierToStr($expr->name, literal: true),
$this->propertyNameToStr($expr->name, literal: true),
$dim,
$value
);
@ -219,7 +219,7 @@ trait PropertyAccessTrait
}
if (!$this->isVarExpr($expr->var)) {
return $this->parseExpr($expr->var) . '.attrRef(' . $this->identifierToStr($expr->name) . ')';
return $this->parseExpr($expr->var) . '.attrRef(' . $this->propertyNameToStr($expr->name) . ')';
}
$objectExpr = $this->parseIdentifier($expr->var);
@ -227,7 +227,7 @@ trait PropertyAccessTrait
$this->fatalError($errorNode, 'Undefined variable `$' . $objectExpr . '`');
}
return $objectExpr . '.attrRef(' . $this->identifierToStr($expr->name) . ')';
return $objectExpr . '.attrRef(' . $this->propertyNameToStr($expr->name) . ')';
}
protected function emitStaticPropertyFetchRef(Expr\StaticPropertyFetch $expr, NodeAbstract $errorNode): string
@ -238,7 +238,7 @@ trait PropertyAccessTrait
}
if ($resolution !== null) {
$property = $this->identifierToStr($expr->name, literal: true);
$property = $this->propertyNameToStr($expr->name, literal: true);
if ($resolution->class !== null) {
$classPtr = $this->getClassEntryPtr($resolution->class);
return Symbol::getStaticPropertyRef() . '(' . $classPtr . ', ' . $property . ')';
@ -386,7 +386,7 @@ trait PropertyAccessTrait
private function parseDynamicStaticPropertyFetch(Expr\StaticPropertyFetch $expr, bool $reference = false): string
{
$classValue = $this->getDynamicStaticClassValue($expr->class);
$propertyValue = $this->identifierToStr($expr->name, literal: true);
$propertyValue = $this->propertyNameToStr($expr->name, literal: true);
$classVar = $this->addTmpVar(Type::VAR);
$propertyVar = $this->addTmpVar(Type::VAR);
@ -452,7 +452,7 @@ trait PropertyAccessTrait
$propertyExpr = null;
if (!$this->isNativePropertyAccess($left) && $this->isVarExpr($left->var)) {
$objectExpr = $this->parseIdentifier($left->var);
$propertyExpr = $this->identifierToStr($left->name, literal: true);
$propertyExpr = $this->propertyNameToStr($left->name, literal: true);
}
if ($this->isIdExpr($left->name)) {
$this->getPropertyIdentifier($left, $left->var, $left->name);
@ -861,7 +861,7 @@ trait PropertyAccessTrait
}
}
return $this->identifierToStr($property, literal: true);
return $this->propertyNameToStr($property, literal: true);
}
private function resolveInstancePropertyFetchTarget(

@ -250,7 +250,7 @@ trait PythonModuleTrait
$writeBack = static fn(string $value): string => $container . '.offsetSet(' . $key . ', ' . $value . ')';
} elseif ($expr->var instanceof Expr\PropertyFetch && $this->isIdExpr($expr->var->name)) {
$receiver = $this->parseOrderedOperand($expr->var->var, false);
$property = $this->identifierToStr($expr->var->name, literal: true);
$property = $this->propertyNameToStr($expr->var->name, literal: true);
$left = $receiver . '.attr(' . $property . ', php::AttrMode::Get)';
$writeBack = static fn(string $value): string => 'typephp_write_property_scoped('
. $receiver . ', ' . $property . ', ' . $value . ', nullptr)';

@ -34,7 +34,10 @@ final class PropertyAccessResolver
if ($class === $parent) {
return true;
}
$class = $this->compiler->getParentClass($class);
// Parent names retain declaration casing. Normalize every hop,
// not only the initial class name.
$class = strtolower(ltrim($this->compiler->getParentClass($class), '\\'));
}
return false;
}

@ -3754,8 +3754,8 @@ CODE;
// Argument unpacking can hide a callback position from the compiler.
// Only that fallback exposes the called class through the user frame;
// ordinary callback resolution uses the explicit CallableScope above.
if ($this->methodDef and $this->methodDef->needsUnpackedCallbackScope) {
$code .= $this->genUnpackedCallbackScopeGuard();
if ($this->context->needsUserCodeCallableScope) {
$code .= $this->genUserCodeCallableScopeGuard();
}
$code .= $stmts;

@ -0,0 +1,128 @@
--TEST--
Scoped callbacks preserve relative, inherited, delayed and nested call contexts
--FILE--
<?php
class ComplexScopeBase
{
private static function privateMap(int $value): string
{
return static::class . ':private:' . $value;
}
protected static function protectedMap(int $value): string
{
return static::class . ':protected:' . $value;
}
public function dynamicRelativeCallbacks(): void
{
$self = 'self';
$static = 'static';
var_dump(array_map([$self, 'privateMap'], [1]));
var_dump(array_map([$static, 'protectedMap'], [2]));
}
public function delayedUnpackedCallback(): Closure
{
return function (int $value): string {
$arguments = [['self', 'privateMap'], $value];
return call_user_func(...$arguments);
};
}
public function unpackedCallbackArray(int $value): string
{
$static = 'static';
$arguments = [[$static, 'protectedMap'], [$value]];
return call_user_func_array(...$arguments);
}
public function nestedDynamicClosure(int $value): string
{
$callback = function (int $innerValue): string {
$arguments = [['self', 'privateMap'], $innerValue];
return call_user_func(...$arguments);
};
return call_user_func($callback, $value);
}
}
class ComplexScopeChild extends ComplexScopeBase
{
public function parentCallback(): void
{
$parent = 'parent';
var_dump(array_map([$parent, 'protectedMap'], [3]));
}
}
class NestedScopeOuter
{
private function invokeInner(int $value): string
{
return (new NestedScopeInner())->run($value) . ':outer';
}
public function callback(): array
{
return [$this, 'invokeInner'];
}
public function run(int $value): string
{
$arguments = [[$this, 'invokeInner'], $value];
return call_user_func(...$arguments);
}
}
class NestedScopeInner
{
private function value(int $value): string
{
return 'inner:' . $value;
}
public function run(int $value): string
{
$arguments = [[$this, 'value'], $value];
return call_user_func(...$arguments);
}
}
function main(): void
{
$object = new ComplexScopeChild();
$object->dynamicRelativeCallbacks();
$object->parentCallback();
$delayed = $object->delayedUnpackedCallback();
var_dump($delayed(4));
var_dump($object->unpackedCallbackArray(5));
var_dump($object->nestedDynamicClosure(6));
$nested = new NestedScopeOuter();
$privateCallback = $nested->callback();
var_dump($nested->run(7));
var_dump(is_callable($privateCallback));
}
?>
--EXPECT--
array(1) {
[0]=>
string(27) "ComplexScopeChild:private:1"
}
array(1) {
[0]=>
string(29) "ComplexScopeChild:protected:2"
}
array(1) {
[0]=>
string(29) "ComplexScopeChild:protected:3"
}
string(26) "ComplexScopeBase:private:4"
string(29) "ComplexScopeChild:protected:5"
string(26) "ComplexScopeBase:private:6"
string(13) "inner:7:outer"
bool(false)

@ -0,0 +1,28 @@
--TEST--
An instance property named static is not treated as late static binding
--FILE--
<?php
class StaticPropertyValue
{
public string $static = 'value';
}
class StaticPropertyReader
{
public function read(?StaticPropertyValue $value): void
{
var_dump($value->static);
var_dump($value?->static);
}
}
function main(): void
{
(new StaticPropertyReader())->read(new StaticPropertyValue());
}
?>
--EXPECT--
string(5) "value"
string(5) "value"
Loading…
Cancel
Save