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
韩天峰 4 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) { foreach ($iterator as $file) {
if ($file->isFile()) { if ($file->isFile()) {
if (self::isPhpFile($file) || self::isNativeSourceFile($file)) { $filePath = $file->getPathname();
$filePath = $file->getPathname(); if (!self::isPhpFile($filePath) && !self::isNativeSourceFile($filePath)) {
} else {
continue; continue;
} }
if (!$this->isExcluded($filePath)) { if (!$this->isExcluded($filePath)) {

@ -23,6 +23,22 @@ use PhpParser\Node\Expr\Variable;
trait ClosureGenerator 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 protected function parseArrowFunction(Expr\ArrowFunction $expr): string
{ {
$nodeFinder = new NodeFinder(); $nodeFinder = new NodeFinder();
@ -211,11 +227,14 @@ trait ClosureGenerator
$this->context = $oriContext; $this->context = $oriContext;
$this->context->beforeStmtLines[] = $code; $this->context->beforeStmtLines[] = $code;
if ($this->methodDef && !$expr->static) { // Even a static closure inherits the outer called scope for late
return 'php::newClosure(' . $tmpVar . ', { ' . implode(', ', $useVars) . ' }, this_)'; // static binding. It still cannot access $this because it was not
} else { // registered in the closure compilation context above.
return 'php::newClosure(' . $tmpVar . ', { ' . implode(', ', $useVars) . ' })'; return $this->genNewClosure(
} $tmpVar,
'{ ' . implode(', ', $useVars) . ' }',
$this->methodDef !== null
);
} }
protected function closureContainsYield(Expr\ArrowFunction|Expr\Closure $expr): bool protected function closureContainsYield(Expr\ArrowFunction|Expr\Closure $expr): bool
@ -325,9 +344,7 @@ trait ClosureGenerator
$code .= $this->getIndent() . '};' . PHP_EOL; $code .= $this->getIndent() . '};' . PHP_EOL;
$args = $capturedArgs ? '{ ' . implode(', ', $capturedArgs) . ' }' : '{}'; $args = $capturedArgs ? '{ ' . implode(', ', $capturedArgs) . ' }' : '{}';
$callback = $this->methodDef $callback = $this->genNewClosure($callbackVar, $args, $this->methodDef !== null);
? 'php::newClosure(' . $callbackVar . ', ' . $args . ', this_)'
: 'php::newClosure(' . $callbackVar . ', ' . $args . ')';
$code .= $this->getIndent() . 'return typephp_new_fiber_generator(' . $callback . ');' . PHP_EOL; $code .= $this->getIndent() . 'return typephp_new_fiber_generator(' . $callback . ');' . PHP_EOL;
return $code; return $code;
} }

@ -301,9 +301,7 @@ trait FiberGenerator
$code .= $this->getIndent() . '};' . PHP_EOL; $code .= $this->getIndent() . '};' . PHP_EOL;
$args = $uses ? '{ ' . implode(', ', $uses) . ' }' : '{}'; $args = $uses ? '{ ' . implode(', ', $uses) . ' }' : '{}';
$closureExpr = $this->class $closureExpr = $this->genNewClosure($closureVar, $args, $this->class !== '');
? 'php::newClosure(' . $closureVar . ', ' . $args . ', this_)'
: 'php::newClosure(' . $closureVar . ', ' . $args . ')';
$code .= $this->getIndent() . 'return typephp_new_fiber_generator(' . $closureExpr . ');' . PHP_EOL; $code .= $this->getIndent() . 'return typephp_new_fiber_generator(' . $closureExpr . ');' . PHP_EOL;
$this->indentLevel--; $this->indentLevel--;
$code .= '}' . PHP_EOL; $code .= '}' . PHP_EOL;

@ -2604,7 +2604,7 @@ CODE;
} }
foreach ($classStmt->traits as $trait1) { foreach ($classStmt->traits as $trait1) {
$traitFullName = $this->getNamespacedClassName($trait1); $traitFullName = $this->getNamespacedClassName($trait1->toString());
if (!$this->hasClass($traitFullName)) { if (!$this->hasClass($traitFullName)) {
$this->fatalError($classStmt, "Trait `{$traitFullName}` not found"); $this->fatalError($classStmt, "Trait `{$traitFullName}` not found");
} }

@ -188,15 +188,20 @@ trait NativeTypeCompatibilityTrait
if (($type === Type::VAR || $type === Type::REF) && $this->isStrictScalarType($argInfo->type)) { if (($type === Type::VAR || $type === Type::REF) && $this->isStrictScalarType($argInfo->type)) {
// A native scalar ABI value has already lost its zval type. Preserve // A native scalar ABI value has already lost its zval type. Preserve
// the dynamic value until strict_types validation has completed. // the dynamic value until strict_types validation has completed.
$tmpVar = $this->addTmpVar(Type::VAR); // Keep the check inside the argument expression: beforeStmtLines
$this->context->beforeStmtLines[] = $tmpVar . ' = (' . $expr . ');'; // may run before an enclosing assignment or comma expression has
$this->context->beforeStmtLines[] = rtrim($this->genStrictScalarParamCheck( // initialized a compiler-generated argument temporary.
$checkedArg = 'typephp_checked_arg';
$check = rtrim($this->genStrictScalarParamCheck(
$argInfo, $argInfo,
$tmpVar, $checkedArg,
$callableName, $callableName,
(string) ($argIndex + 1) (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); $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