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
pull/45/head
韩天峰 3 weeks ago
parent db4b7f9e28
commit 8dd33c1400
  1. 5
      src/Build/FileScanner.php
  2. 33
      src/Generator/ClosureGenerator.php
  3. 4
      src/Generator/FiberGenerator.php
  4. 2
      src/Translator.php
  5. 15
      src/TypeSystem/NativeTypeCompatibilityTrait.php
  6. 55
      tests/compiler/closure/lexical-scope.phpt

@ -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)) {

@ -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;
}

@ -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;

@ -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");
}

@ -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);

@ -0,0 +1,55 @@
--TEST--
closures preserve lexical class scope and late static binding scope
--FILE--
<?php
class ClosureScopeParent
{
private static string $secret = 'private';
public function callbacks(): array
{
return [
fn(): string => 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
Loading…
Cancel
Save