Skip to content

fix(compiler): preserve runtime class dispatch and introspection for polymorphic objects - #110

Merged
matyhtf merged 3 commits into
swoole:masterfrom
prateekbhujel:prateek/preserve-runtime-class-dispatch
Sep 14, 2026
Merged

fix(compiler): preserve runtime class dispatch and introspection for polymorphic objects#110
matyhtf merged 3 commits into
swoole:masterfrom
prateekbhujel:prateek/preserve-runtime-class-dispatch

Conversation

@prateekbhujel

@prateekbhujel prateekbhujel commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

When an object comes from a function or method return (like $a = getAnimal()), SSA marks it in stableObjects. But that only tracks the upper-bound declared type, not the actual runtime subclass:

  • get_class($a) was getting folded at compile time to the base class instead of returning the runtime subclass (and returned a string on null instead of throwing TypeError)
  • $a::who() statically resolved to the base class, bypassing subclass overrides

Swapped isStableObject with isset($this->context->exactObjects[...]) in genGetClassOptimized and parseStaticCall. If the class isn't proven exact via new ConcreteClass(), it safely falls back to runtime dispatch (php::fn::get_class and php::callStaticMethod).

Added unit tests for both exact and polymorphic cases.

…polymorphic objects

When an object variable has an upper-bound declared type (e.g. assigned from
a function or method return), SSA marks it as a stable object. However,
stableObjects does not guarantee the concrete runtime class: the instance
may be a subclass at runtime, or null if nullable.

1. FuncCallOptimizer::genGetClassOptimized checked isStableObject($obj->name)
   and folded get_class($obj) to the declared type literal string. For
   $a = getAnimal() returning a Dog, get_class($a) was incorrectly folded
   to "Animal" at compile time.
2. MethodCallTrait::parseStaticCall checked isStableObject($class) and
   jumped to _do_call with the declared base class. For $a::who(), this
   statically invoked Base::who() instead of dispatching to Dog::who() on
   the runtime object.

Require proven exact object instances (exactObjects, populated from direct
new ConcreteClass() definitions) before folding get_class() or devirtualizing
static calls on object variables. Non-exact variables fall back to runtime
class resolution (php::fn::get_class() and php::callStaticMethod()),
matching PHP semantics.

Add unit test coverage verifying exact objects continue to fold/devirtualize
while polymorphic variables retain dynamic runtime dispatch.
@prateekbhujel
prateekbhujel force-pushed the prateek/preserve-runtime-class-dispatch branch from e1e645c to 2a8f442 Compare September 12, 2026 12:54

@matyhtf matyhtf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the incorrect devirtualization. Public polymorphic get_class() and $object::method() dispatch work in the tested cases, but two runtime issues still need to be fixed.

  1. The new dynamic $object::method() path loses the lexical class scope. A protected static call that is valid in PHP is treated as coming from global scope:
class Base {
    protected static function identify(): string {
        return static::class;
    }

    public static function exercise(): string {
        $object = makeChild(); // declared Base, returns Child
        return $object::identify();
    }
}

PHP returns the child class name, while the compiled program throws:

Call to protected method Base::identify() from global scope

The runtime dispatch must preserve both the runtime target class and the lexical callable scope. Emitting an unscoped php::callStaticMethod() is insufficient here.

  1. The PR description says nullable values now produce PHP's TypeError, but get_class() on a nullable object still segfaults. The non-exact path emits php::fn::get_class(value), whose current fast path dereferences obj.ce() without validating null. For example, a ?Base return containing null causes SIGSEGV instead of a catchable TypeError. Please use a checked/runtime path for values that may be null, or make the helper enforce PHP's argument semantics.

The added PHPUnit test only checks generated code text, so neither runtime failure is covered. Please add behavioral PHPT coverage for a protected polymorphic static call and nullable get_class(), in addition to the public dispatch cases.

…time type checks for get_class

- Use php::callScoped with getCallableScopeExpr() for dynamic static calls inside class methods to preserve caller visibility on protected methods
- Fall back to runtime php::call for get_class on non-exact values so nullable objects throw TypeError instead of dereferencing null
- Add behavioral PHPT tests for protected polymorphic static calls and nullable get_class
- Update unit tests for scoped dispatch and runtime introspection
@prateekbhujel

Copy link
Copy Markdown
Contributor Author

Updated. Dynamic static calls inside methods now use php::callScoped to keep caller scope for protected methods, and non-exact get_class falls back to runtime php::call so null throws TypeError. Added the requested phpt tests for both as well.

Details on the changes:

  1. Static call scope:
    In MethodCallTrait::parseStaticCall(), when dynamic static dispatch occurs inside a method ($this->methodDef !== null), we emit php::callScoped($fn, $this->getCallableScopeExpr(), ...). This initializes CallableScope with the executing class frame so Zend passes the access check for protected methods, while keeping $fn dynamically target-bound (Child::identify) so late static binding (static::class) resolves the runtime subclass. Top-level/global callers continue using php::callStaticMethod().

  2. Nullable get_class() handling:
    In FuncCallOptimizer::genGetClassOptimized(), non-exact objects now return false instead of emitting the unchecked C++ helper php::fn::get_class(). This routes execution through genRuntimeFunctionCall() (php::call targeting Zend's zif_get_class), which correctly returns the runtime subclass for objects and triggers standard PHP TypeError on null without segfaulting. Proven exact instances (new Foo()) continue to fold at compile time.

  3. Behavioral PHPT coverage:

    • tests/compiler/static/polymorphic-static-call-scope.phpt: verifies protected static method dispatch on polymorphic instances through static and instance caller methods, subclass overrides, and global public calls.
    • tests/compiler/stdlib/get_class_nullable.phpt: verifies runtime class introspection on nullable objects returning null (catching TypeError), literal get_class(null), exact objects, and polymorphic objects.
    • Updated unit assertions in phpunit/src/PolymorphicClassDispatchTest.php.

In TypePHP AOT compilation, function main(): void serves as the binary
entrypoint. Top-level function calls outside a function are rejected by
the preprocessor as stray code.
@matyhtf
matyhtf merged commit 13889e9 into swoole:master Sep 14, 2026
14 checks passed
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.

2 participants