diff --git a/docs/SCOPE_MANAGEMENT.md b/docs/SCOPE_MANAGEMENT.md index 9e8d006e..e2944748 100644 --- a/docs/SCOPE_MANAGEMENT.md +++ b/docs/SCOPE_MANAGEMENT.md @@ -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` 类。 diff --git a/phpunit/src/Context/FunctionContextTest.php b/phpunit/src/Context/FunctionContextTest.php index a8f14828..02275b1e 100644 --- a/phpunit/src/Context/FunctionContextTest.php +++ b/phpunit/src/Context/FunctionContextTest.php @@ -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 diff --git a/phpunit/src/Entity/MethodDefExtendedTest.php b/phpunit/src/Entity/MethodDefExtendedTest.php index 8425e841..75341663 100644 --- a/phpunit/src/Entity/MethodDefExtendedTest.php +++ b/phpunit/src/Entity/MethodDefExtendedTest.php @@ -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'); diff --git a/phpunit/src/Entity/MethodDefTest.php b/phpunit/src/Entity/MethodDefTest.php index 5e67f945..f379533f 100644 --- a/phpunit/src/Entity/MethodDefTest.php +++ b/phpunit/src/Entity/MethodDefTest.php @@ -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 diff --git a/phpunit/src/Resolver/PropertyAccessResolverTest.php b/phpunit/src/Resolver/PropertyAccessResolverTest.php new file mode 100644 index 00000000..b2ef5563 --- /dev/null +++ b/phpunit/src/Resolver/PropertyAccessResolverTest.php @@ -0,0 +1,44 @@ + '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')); + } +} diff --git a/phpunit/src/TraitsTest.php b/phpunit/src/TraitsTest.php index e6bff789..4bb014e5 100644 --- a/phpunit/src/TraitsTest.php +++ b/phpunit/src/TraitsTest.php @@ -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, ); } diff --git a/src/CompilerBase.php b/src/CompilerBase.php index a7b11879..f48e9d3d 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -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)) { diff --git a/src/Context/FunctionContext.php b/src/Context/FunctionContext.php index 865d5a71..baa227eb 100644 --- a/src/Context/FunctionContext.php +++ b/src/Context/FunctionContext.php @@ -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. */ diff --git a/src/Entity/MethodDef.php b/src/Entity/MethodDef.php index 1af38ee2..3818397c 100644 --- a/src/Entity/MethodDef.php +++ b/src/Entity/MethodDef.php @@ -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 diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index d805f88e..00b85b15 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -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() . ')'; } diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index 9cd5a0c3..e194c13b 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -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(); diff --git a/src/Generator/FiberGenerator.php b/src/Generator/FiberGenerator.php index d1054b5c..6466647c 100644 --- a/src/Generator/FiberGenerator.php +++ b/src/Generator/FiberGenerator.php @@ -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; diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index 2e9c5fb9..2c2b369e 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -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); diff --git a/src/Parser/NullsafeAccessTrait.php b/src/Parser/NullsafeAccessTrait.php index 1920f2bd..efa203fa 100644 --- a/src/Parser/NullsafeAccessTrait.php +++ b/src/Parser/NullsafeAccessTrait.php @@ -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]; diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index 377c220d..cd3a5a86 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -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( diff --git a/src/Python/PythonModuleTrait.php b/src/Python/PythonModuleTrait.php index e29581ca..00adbc57 100644 --- a/src/Python/PythonModuleTrait.php +++ b/src/Python/PythonModuleTrait.php @@ -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)'; diff --git a/src/Resolver/PropertyAccessResolver.php b/src/Resolver/PropertyAccessResolver.php index 8ae4263d..c720115c 100644 --- a/src/Resolver/PropertyAccessResolver.php +++ b/src/Resolver/PropertyAccessResolver.php @@ -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; } diff --git a/src/Translator.php b/src/Translator.php index 321d5dbb..68af7914 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -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; diff --git a/tests/compiler/callable/scoped-callback-complex.phpt b/tests/compiler/callable/scoped-callback-complex.phpt new file mode 100644 index 00000000..341ca7d5 --- /dev/null +++ b/tests/compiler/callable/scoped-callback-complex.phpt @@ -0,0 +1,128 @@ +--TEST-- +Scoped callbacks preserve relative, inherited, delayed and nested call contexts +--FILE-- +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) diff --git a/tests/compiler/object_property/property-named-static.phpt b/tests/compiler/object_property/property-named-static.phpt new file mode 100644 index 00000000..f89b7a2f --- /dev/null +++ b/tests/compiler/object_property/property-named-static.phpt @@ -0,0 +1,28 @@ +--TEST-- +An instance property named static is not treated as late static binding +--FILE-- +static); + var_dump($value?->static); + } +} + +function main(): void +{ + (new StaticPropertyReader())->read(new StaticPropertyValue()); +} + +?> +--EXPECT-- +string(5) "value" +string(5) "value"