From 8dd33c1400e72f34151862a706cb19aaf5406ba4 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 7 Aug 2026 12:43:18 +0800 Subject: [PATCH] feat(closure): implement proper lexical scope handling for closures - Add genNewClosure method to handle closure creation with correct scope - Preserve lexical class scope and late static binding for closures - Handle trait method flattening correctly when creating closures in traits - Update arrow function and fiber generator closure creation to use new method - Fix strict scalar parameter checking to run inside argument expressions - Correct trait name resolution in class definition processing - Add comprehensive test coverage for closure lexical scope behavior --- src/Build/FileScanner.php | 5 +- src/Generator/ClosureGenerator.php | 33 ++++++++--- src/Generator/FiberGenerator.php | 4 +- src/Translator.php | 2 +- .../NativeTypeCompatibilityTrait.php | 15 +++-- tests/compiler/closure/lexical-scope.phpt | 55 +++++++++++++++++++ 6 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 tests/compiler/closure/lexical-scope.phpt diff --git a/src/Build/FileScanner.php b/src/Build/FileScanner.php index 291b8300..6828b920 100644 --- a/src/Build/FileScanner.php +++ b/src/Build/FileScanner.php @@ -90,9 +90,8 @@ class FileScanner foreach ($iterator as $file) { if ($file->isFile()) { - if (self::isPhpFile($file) || self::isNativeSourceFile($file)) { - $filePath = $file->getPathname(); - } else { + $filePath = $file->getPathname(); + if (!self::isPhpFile($filePath) && !self::isNativeSourceFile($filePath)) { continue; } if (!$this->isExcluded($filePath)) { diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index c93281cd..c2311824 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -23,6 +23,22 @@ use PhpParser\Node\Expr\Variable; trait ClosureGenerator { + protected function genNewClosure(string $callback, string $uses, bool $hasThis): string + { + $thisArg = $hasThis ? 'this_' : '{}'; + if ($this->classDef?->trait !== null) { + // 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_get_called_ce(this_)'; + } else { + $scope = $this->class + ? $this->getClassEntryPtr($this->getFullClassName()) + : 'nullptr'; + } + return 'php::newClosure(' . $callback . ', ' . $uses . ', ' . $thisArg . ', ' . $scope . ')'; + } + protected function parseArrowFunction(Expr\ArrowFunction $expr): string { $nodeFinder = new NodeFinder(); @@ -211,11 +227,14 @@ trait ClosureGenerator $this->context = $oriContext; $this->context->beforeStmtLines[] = $code; - if ($this->methodDef && !$expr->static) { - return 'php::newClosure(' . $tmpVar . ', { ' . implode(', ', $useVars) . ' }, this_)'; - } else { - return 'php::newClosure(' . $tmpVar . ', { ' . implode(', ', $useVars) . ' })'; - } + // Even a static closure inherits the outer called scope for late + // static binding. It still cannot access $this because it was not + // registered in the closure compilation context above. + return $this->genNewClosure( + $tmpVar, + '{ ' . implode(', ', $useVars) . ' }', + $this->methodDef !== null + ); } protected function closureContainsYield(Expr\ArrowFunction|Expr\Closure $expr): bool @@ -325,9 +344,7 @@ trait ClosureGenerator $code .= $this->getIndent() . '};' . PHP_EOL; $args = $capturedArgs ? '{ ' . implode(', ', $capturedArgs) . ' }' : '{}'; - $callback = $this->methodDef - ? 'php::newClosure(' . $callbackVar . ', ' . $args . ', this_)' - : 'php::newClosure(' . $callbackVar . ', ' . $args . ')'; + $callback = $this->genNewClosure($callbackVar, $args, $this->methodDef !== null); $code .= $this->getIndent() . 'return typephp_new_fiber_generator(' . $callback . ');' . PHP_EOL; return $code; } diff --git a/src/Generator/FiberGenerator.php b/src/Generator/FiberGenerator.php index 9aa44dbd..1fbeba39 100644 --- a/src/Generator/FiberGenerator.php +++ b/src/Generator/FiberGenerator.php @@ -301,9 +301,7 @@ trait FiberGenerator $code .= $this->getIndent() . '};' . PHP_EOL; $args = $uses ? '{ ' . implode(', ', $uses) . ' }' : '{}'; - $closureExpr = $this->class - ? 'php::newClosure(' . $closureVar . ', ' . $args . ', this_)' - : 'php::newClosure(' . $closureVar . ', ' . $args . ')'; + $closureExpr = $this->genNewClosure($closureVar, $args, $this->class !== ''); $code .= $this->getIndent() . 'return typephp_new_fiber_generator(' . $closureExpr . ');' . PHP_EOL; $this->indentLevel--; $code .= '}' . PHP_EOL; diff --git a/src/Translator.php b/src/Translator.php index 5bb343b2..c1bb45b7 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -2604,7 +2604,7 @@ CODE; } foreach ($classStmt->traits as $trait1) { - $traitFullName = $this->getNamespacedClassName($trait1); + $traitFullName = $this->getNamespacedClassName($trait1->toString()); if (!$this->hasClass($traitFullName)) { $this->fatalError($classStmt, "Trait `{$traitFullName}` not found"); } diff --git a/src/TypeSystem/NativeTypeCompatibilityTrait.php b/src/TypeSystem/NativeTypeCompatibilityTrait.php index 89fe0ca7..4fd9c467 100644 --- a/src/TypeSystem/NativeTypeCompatibilityTrait.php +++ b/src/TypeSystem/NativeTypeCompatibilityTrait.php @@ -188,15 +188,20 @@ trait NativeTypeCompatibilityTrait if (($type === Type::VAR || $type === Type::REF) && $this->isStrictScalarType($argInfo->type)) { // A native scalar ABI value has already lost its zval type. Preserve // the dynamic value until strict_types validation has completed. - $tmpVar = $this->addTmpVar(Type::VAR); - $this->context->beforeStmtLines[] = $tmpVar . ' = (' . $expr . ');'; - $this->context->beforeStmtLines[] = rtrim($this->genStrictScalarParamCheck( + // Keep the check inside the argument expression: beforeStmtLines + // may run before an enclosing assignment or comma expression has + // initialized a compiler-generated argument temporary. + $checkedArg = 'typephp_checked_arg'; + $check = rtrim($this->genStrictScalarParamCheck( $argInfo, - $tmpVar, + $checkedArg, $callableName, (string) ($argIndex + 1) )); - $expr = $tmpVar; + $expr = '([&](php::Var ' . $checkedArg . ') -> php::Var {' . PHP_EOL + . $check . PHP_EOL + . $this->getIndent() . 'return ' . $checkedArg . ';' . PHP_EOL + . $this->getIndent() . '})(' . $expr . ')'; } $this->checkVarAssignExpr($arg, $argInfo->type, $type); diff --git a/tests/compiler/closure/lexical-scope.phpt b/tests/compiler/closure/lexical-scope.phpt new file mode 100644 index 00000000..25db5c17 --- /dev/null +++ b/tests/compiler/closure/lexical-scope.phpt @@ -0,0 +1,55 @@ +--TEST-- +closures preserve lexical class scope and late static binding scope +--FILE-- + self::$secret, + static fn(): string => self::$secret, + static fn(): string => static::class, + ]; + } +} + +class ClosureScopeChild extends ClosureScopeParent +{ +} + +trait ClosureScopeTrait +{ + public function traitCallback(): Closure + { + return static fn(): string => self::$traitSecret; + } +} + +class ClosureScopeTraitUser +{ + use ClosureScopeTrait; + + private static string $traitSecret = 'trait-private'; +} + +function main(): void +{ + foreach ((new ClosureScopeChild())->callbacks() as $callback) { + $scope = (new ReflectionFunction($callback))->getClosureScopeClass(); + echo $scope?->getName(), ':', $callback(), "\n"; + } + + $traitCallback = (new ClosureScopeTraitUser())->traitCallback(); + $traitScope = (new ReflectionFunction($traitCallback))->getClosureScopeClass(); + echo $traitScope?->getName(), ':', $traitCallback(), "\n"; +} +?> +--EXPECT-- +ClosureScopeParent:private +ClosureScopeParent:private +ClosureScopeParent:ClosureScopeChild +ClosureScopeTraitUser:trait-private