Skip to content

fix(introspection): hide compiler-internal members, skip virtual properties in var_dump, add ReflectionType and SessionHandlerInterface - #1923

Open
JGamboa wants to merge 9 commits into
illegalstudio:mainfrom
JGamboa:fix/introspection-hidden-internals
Open

JGamboa wants to merge 9 commits into
illegalstudio:mainfrom
JGamboa:fix/introspection-hidden-internals

Conversation

@JGamboa

@JGamboa JGamboa commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Introspection fixes split out of #1886 (the "introspection" PR of the suggested split). Each one makes elephc report what PHP reports:

  • Reflection backing slots and hook accessors. Reflection listed the ~36 private __* storage slots elephc declares on the builtin Reflection classes in getProperties() / hasProperty() / getDefaultProperties(), and listed the generated __propget_* / __propset_* accessors of hooked properties in getMethods() / hasMethod(). Reflecting a user subclass of ReflectionClass and calling getProperties() exhausted the default heap. The statically expanded getProperties() (receiver known at lowering time) also listed a parent's private properties. One rule, ClassInfo::reflection_lists_property, now serves the codegen metadata and the static EIR expansion.
  • Internal classes in get_declared_classes(). __ElephcWeakMapIterator, declared by the WeakMap prelude as a program statement, was listed. Code that instantiates every declared class (php-src Zend/tests/instantiate_all_classes.phpt) then hit a fatal error PHP never raises.
  • Virtual hooked properties in var_dump(). A property whose hooks never touch a backing slot was printed as ["name"]=> uninitialized(string), read from an unrelated layout offset. PHP does not print it, and the object header count already excluded it. print_r() already omitted these properties.
  • ReflectionType. It is now the abstract parent of ReflectionNamedType, ReflectionUnionType and ReflectionIntersectionType, so ?ReflectionType hints, instanceof ReflectionType and (string) $type compile.
  • SessionHandlerInterface outside --web. Until now only the --web prelude declared it, so a handler class failed with "Unknown interface". The checker now injects the interface unless an internal prelude already declared it (the --web declaration still wins). A user redeclaration is rejected like the other builtin interfaces.

Closes #1900
Part of #855 (ReflectionType only; Reflector, ReflectionGenerator and ReflectionFunctionAbstract are still missing)

Repro (no issue yet)

Reflection backing slots and hook accessors:

<?php
class Mine extends ReflectionClass {}
$rc = new ReflectionClass('Mine');
var_dump(count($rc->getMethod('getName')->getDeclaringClass()->getDefaultProperties())); // main: int(36), PHP: int(0)
foreach ($rc->getProperties() as $p) {}                                                    // main: Fatal error: heap memory exhausted

class Hooked { public string $label { get => "x"; set { } } public function run(): void {} }
foreach ((new ReflectionClass('Hooked'))->getMethods() as $m) echo $m->getName(), ",";    // main: __propget_label,__propset_label,run,  PHP: run,

Virtual hooked property:

<?php
class A { private string $n = 'x'; public string $name { get => $this->n; } }
var_dump(new A()); // main also prints ["name"]=> uninitialized(string)

Not in this PR

Tests

This branch is based on main @ df57a98. Each test below fails on that main (checked with a main build) and passes on this branch:

  • codegen::oop::reflection_builtin_subclass::{reflection_class_backing_slots_are_not_reflected, property_hook_accessors_are_not_reflected_as_methods, static_get_properties_hides_reflection_backing_slots, static_get_properties_skips_inherited_private_properties}
  • codegen::core_classes::test_declared_classes_hide_internal_core_prelude_classes
  • var_dump_object_tests::virtual_hooked_property_is_not_rendered
  • codegen::oop::reflection_functions::test_reflection_type_is_the_common_parent (main: "Unknown type: ReflectionType")
  • codegen::core_classes::test_session_handler_interface_is_implementable_without_web (main: "Unknown interface: SessionHandlerInterface")
  • error_tests::test_error_session_handler_interface_cannot_be_redeclared (main accepts the user interface)

The control test reflecting_a_reflection_class_subclass_fits_the_default_heap passes on main too. It is kept because it guards the heap budget once the Reflection classes gain public properties.

The builtin docs and symbol registries were regenerated with scripts/docs (two catalog entries changed).

Refs #1886

AI assistance

This PR was prepared with substantial help from Claude Code (Anthropic). The changes were split out of #1886, re-based on current main and reviewed by me. Every regression test was run against a main build and against this branch.

🤖 Generated with Claude Code

@github-actions github-actions Bot added area:codegen Touches target-aware assembly or backend lowering. area:eir Touches EIR definitions, lowering, validation, or passes. area:types Touches type checking, inference, or compatibility. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:m Medium-sized pull request. type:fix Corrects broken or incompatible behavior. labels Oct 19, 2026
@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; the remaining trait-name conflict is fixed.

Summary

The PR hides compiler-only members from reflection, skips virtual properties in var_dump(), adds ReflectionType, and makes SessionHandlerInterface available outside --web.

  • Reflection lists PHP members instead of compiler-created members.
  • var_dump() leaves virtual hooked properties out of object output.
  • Declared class lists leave out the internal WeakMap iterator.
  • Reflection type results share the abstract ReflectionType parent.
  • SessionHandlerInterface is available outside web compilation too.

Reviews (4) · Last reviewed commit: "fix(checker): reject a trait named Sessi..." · Reviewed by Greptile

Comment thread src/ir_lower/program/declaration_metadata.rs Outdated
Comment thread crates/elephc-builtin-contract/src/catalog_classes.rs
…sors

PHP's Reflection classes declare no private properties, and property-hook
accessors are not PHP methods, but elephc's Reflection listed both:

- `getProperties()`, `hasProperty()` and `getDefaultProperties()` on a
  builtin Reflection class (or a user subclass of one) listed the ~36
  private `__*` storage slots elephc declares on it. Since every
  ReflectionMethod embeds a declaring-class ReflectionClass,
  `(new ReflectionClass('Mine'))->getProperties()` over
  `class Mine extends ReflectionClass {}` exhausted the default heap.
- `getMethods()` and `hasMethod()` listed the generated
  `__propget_*` / `__propset_*` accessors of hooked properties.
- the statically expanded `getProperties()` (receiver known at lowering
  time) applied no visibility rule at all, so it also listed a parent's
  private properties, which PHP never reports.

The property rule now lives in `ClassInfo::reflection_lists_property` and is
shared by the codegen metadata and the static EIR expansion; method lists
skip hook accessors, resolved on the declaring class. Reflection inside
eval() fragments does not apply these filters yet (documented).
…red_classes()

The WeakMap prelude declares `__ElephcWeakMapIterator` as a program
statement, so the source-order half of the declared-class list exposed it:

    $map = new WeakMap();
    var_dump(in_array('__ElephcWeakMapIterator', get_declared_classes()));
    // bool(true) before, bool(false) in PHP

Code that instantiates every declared class (php-src
Zend/tests/instantiate_all_classes.phpt) then reached the internal iterator
without its map and stopped on "Typed property
__ElephcWeakMapIterator::$__elephc_map must not be accessed before
initialization". The source-order names now go
through the same internal-name filter as the registered ones.
A virtual hooked property (hooks that never touch a backing slot) has no
storage, and PHP's var_dump() does not render it. elephc emitted a row for
it that read the layout's fallback offset:

    class A {
        private string $n = 'x';
        public string $name { get => $this->n; }
    }
    var_dump(new A());
    // elephc printed ["name"]=> uninitialized(string) after ["n":"A":private]

The `_class_vd_desc_*` descriptor rows now skip virtual properties, both in
the declared property table and in the __debugInfo projection, so the rows
agree with the object header count. print_r() already omitted them.

var_export() is unchanged: PHP 8.5 renders virtual properties there with
their get-hook values (`'name' => 'x'`), and elephc omits them before and
after this change. That is a separate gap.
PHP's ReflectionNamedType, ReflectionUnionType and
ReflectionIntersectionType extend the abstract ReflectionType, and code
typed against it (`?ReflectionType $t`, `$t instanceof ReflectionType`) is
common in libraries that inspect signatures. elephc had no ReflectionType,
so such code failed to compile with "Unknown type: ReflectionType".

ReflectionType is now injected with the other Reflection classes, the three
concrete type classes extend it, and the class catalog lists it. The shell
declares no members yet, so `allowsNull()` / `__toString()` are still called
through the concrete classes (documented); a call on a ReflectionType-typed
receiver stays a compile-time "Undefined method" rather than a dispatch the
Reflection reachability scan cannot see.
Only the `--web` prelude declared SessionHandlerInterface, so a handler
class, or any code typed against the interface, failed to compile in a
normal build ("Unknown interface: SessionHandlerInterface"), although PHP's
CLI has it.

The checker now injects the interface (open/close/read/write/destroy/gc with
PHP's signatures) unless an internal prelude already declared it, so the
`--web` declaration keeps winning. A user declaration of the name is
rejected as "Cannot redeclare built-in interface: SessionHandlerInterface",
like the other builtin interfaces. The class catalog now routes it as
checker-injected.
…declared lists

The source-order filter used the `__elephc` prefix, which also hid user
classes and interfaces such as `__ElephcUser`. PHP does not reserve that
prefix, so filter user-position names by the builtin class contract's
`internal` flag instead.
…de --web

Extend the reflection example with a helper typed against the abstract
ReflectionType parent, and add a session-handler example that implements
SessionHandlerInterface in a plain CLI program.
@JGamboa
JGamboa force-pushed the fix/introspection-hidden-internals branch from fa59274 to 45e9c83 Compare October 21, 2026 06:04
Comment thread src/types/checker/builtin_session_interfaces.rs
The builtin SessionHandlerInterface injection only looked for a
colliding class or interface name, so outside --web a user trait of that
name was accepted next to the injected interface. PHP rejects it.
Include the declared trait names, as the Reflection injection already
does, and point the error at the trait declaration.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:codegen Touches target-aware assembly or backend lowering. area:eir Touches EIR definitions, lowering, validation, or passes. area:types Touches type checking, inference, or compatibility. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:m Medium-sized pull request. type:fix Corrects broken or incompatible behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

get_declared_classes() lists the internal prelude class __ElephcWeakMapIterator

1 participant