Skip to content

Language: self::/parent:: static FCC preserves late static binding (#27835) - #27844

Merged
PurHur merged 1 commit into
masterfrom
agent/issue-27835-self-fcc-lsb
Aug 5, 2026
Merged

PurHur merged 1 commit into
masterfrom
agent/issue-27835-self-fcc-lsb

Conversation

@PurHur

@PurHur PurHur commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fix self::method(...) / parent::method(...) first-class callables created in a static method so invoke preserves creation-time late static binding (Zend zend_closures.c).
  • Root cause: compiler baked self/parent to a FQCN for AOT/JIT and dropped the keyword; VM then set calledClass from that FQCN, so B::viaSelf() with self::foo(...) printed A:s instead of B:s. Named A::foo(...) correctly stays frozen to A.
  • Keep fromCallableScope, store boundCalledScopeClass from the creating frame, and use closureCalledScopeClass() on wrappedFunc invoke (Language: self::method(...) FCC from child loses late static binding — static::class is parent (Zend/zend_closures.c) #27835).

Closes #27835

php-src reference

  • Zend/zend_closures.c — fake closure called_scope
  • Zend/zend_compile.c — ZEND_AST_CALLABLE_CONVERT / self:: FCC

Test plan

  • Issue repro (Zend vs VM / PROFILE=8.4):
./script/docker-exec.sh -- bash -lc 'cat > /tmp/fcc_self_lsb.php <<'"'"'PHP'"'"'
<?php
class A {
  public static function foo($x) { return static::class.":$x"; }
  public static function viaSelf() {
    $f = self::foo(...);
    return $f("s");
  }
  public static function viaNamed() {
    $f = A::foo(...);
    return $f("s");
  }
}
class B extends A {}
echo "Aself=", A::viaSelf(), "\n";
echo "Bself=", B::viaSelf(), "\n";
echo "Anamed=", A::viaNamed(), "\n";
echo "Bnamed=", B::viaNamed(), "\n";
PHP
php /tmp/fcc_self_lsb.php
php bin/vm.php /tmp/fcc_self_lsb.php
PHP_COMPILER_PROFILE=8.4 php bin/vm.php /tmp/fcc_self_lsb.php'

Observed: Aself=A:s / Bself=B:s / Anamed=A:s / Bnamed=A:s (exit 0).

  • parent:: static FCC LSB (C::viaParent() → C:s) also fixed by the same path
  • vendor/bin/phpunit --filter FirstClassCallableTest::testVmSelfStatic → OK
  • vendor/bin/phpunit --filter first_class_callable_self_static_lsb test/compliance/ClosureVMTest.php → OK
  • Related FCC compliance (parent_method, parent_method_args, parent_static) → OK
  • Gate snapshot: bootstrap-inventory --check OK 7020/7020; release-readiness completed (exit 0)
  • Not covered: JIT/AOT still bake FQCN without runtime called_scope capture (VM php-src-strict for this issue)

Made with Cursor

…27835)

Keep fromCallableScope when baking self/parent to FQCN, store creation
called_scope on the Closure, and use it on invoke so B::viaSelf() with
self::foo(...) yields B (Zend), not declaring class A.

Co-authored-by: Cursor <cursoragent@cursor.com>
@PurHur
PurHur merged commit b4c2055 into master Aug 5, 2026
1 check failed
@PurHur
PurHur deleted the agent/issue-27835-self-fcc-lsb branch August 5, 2026 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Language: self::method(...) FCC from child loses late static binding — static::class is parent (Zend/zend_closures.c)

1 participant