Repository navigation
Conversation
Restores compilation of interface methods with @template, keeps interface conformance and override dispatch for templated methods, demotes broken docblock covariance markers, erases docblock type arguments on undeclared (builtin) interfaces, and adds the ELEPHC_PHPDOC_GENERICS=off escape hatch. Regression from illegalstudio#1221.
- Leave an empty marker interface in each class/interface template's place and make every instantiation inherit it, so `instanceof Coll` holds for every instantiation and subclass; an instanceof guard on a template does not narrow to the member-less marker. - A method template bounded by the native class (`T of Model` on `Model`) refines it; a template no position adopts is dropped; a native `mixed` is no longer replaced by PHPDoc. - Erased `array<mixed, mixed>`/`mixed[]` become plain `array`, keeping the physical signature of a dispatched method compatible. - Methods implementing built-in interfaces (ArrayAccess, Countable, IteratorAggregate, JsonSerializable, ...) count as dispatched; any other undeclared ancestor makes every method dispatched. - ELEPHC_PHPDOC_GENERICS accepts off|0|false in any case.
…nerics follow-ups
- InterfaceInfo::reflection_hidden marks the marker interface a generic CLASS template leaves behind; interface_exists, get_declared_interfaces and class_implements skip it, and is_subclass_of(x, 'Box') holds only for a class extending an instantiation, as in php-src. instanceof is unchanged; interface-template markers stay visible. - An instanceof guard on a template keeps an object local's class and reads anything else as mixed, so a store under the guard merges as under any other instanceof; the mixed-storage scan no longer treats it as a narrowing.
… dispatch ABI - overrides may make a parent-required parameter optional and rename the variadic - union returns are covariant member by member, including the class under construction (Node|bool -> Leaf|false), guarded by an unchanged physical return representation - #[\ReturnTypeWillChange] relaxes only internal parents/interfaces, and only when the relaxed return keeps the parent's return ABI (or an interface boxes into mixed) - untyped __toString() gets PHP's implicit ': string' where Stringable or a declared parent __toString() requires it - trailing optional parameters added by an override are refused with a dedicated diagnostic: fixed-arity dispatch cannot supply the added defaults yet
A vtable call is typed and compiled against the receiver's declared class but runs whichever override the object carries. An untyped abstract method took the LAST child's inferred return (propagate_abstract_return_types), and untyped overrides boxed params/returns as Mixed while the slot said int, so `$shape->tag(1)` printed a heap pointer. - types::override_families groups classes sharing a dispatched method. - Checker: placeholders take the type all implementations agree on; when they disagree, inferred returns take a covering union or mixed, inside the method fixed point so callers re-infer from it. - EIR: every family shares parameter and return representations (Mixed where they differ); declared-float/by-ref/generator widenings are refused. - Late-static result typing never overrides a boxed call ABI.
…prop)
The parser desugars $expr::NAME / $expr::$prop into the internal
__elephc_dynamic_class_constant / __elephc_dynamic_static_property primitives;
ir_lower expands them into a case-insensitive runtime class-name dispatch over
the AOT candidates, each arm lowering the ordinary named node. $this::X maps to
static::X. Dynamic static-property writes and computed $class::{expr} fetches
are rejected with a clear compile error. Reachability treats a non-literal
receiver as a dynamic-class hazard; --strict-php accepts the desugared call.
A statement that failed after consuming its closing brace or semicolon used to force-skip one more token (the parent's '}' or the next statement's keyword), desynchronizing the rest of the file; a failure mid array literal resumed inside the literal. recover_after_failed_stmt skips dangling catch/finally/else clauses, keeps clean boundaries, and seeds the bracket depth from the failed statement.
…ditions - #[Attr] const X = 1; (PHP 8.5) attaches the attributes to the ConstDecl; a const list keeps PHP's 'Cannot apply attributes to multiple constants at once' error. - while ([$a, $b] = expr) lowers to while (true) with a head that binds the value, unpacks it (or nulls every target when it is not an array or object, as PHP does) and breaks when the value is falsy.
…perator PHP accepts any expression as a statement, e.g. '0 > $T && $T += 0x40;' (symfony/polyfill-intl-normalizer). Literal, predefined/magic constant and prefix-operator tokens at statement position now go to the expression statement parser instead of 'Unexpected token at statement position'.
…s exist - class overrides must keep every parameter's physical representation; an untyped parameter cannot replace a declared one (typed later from call sites) - renamed variadics must keep the element representation and stay contravariant - overrides and implementations cannot change an inherited default value (callers fill the static type's default) - #[\ReturnTypeWillChange] on a class override always needs a declared return with the parent's representation - the implicit __toString(): string is not applied when a subclass overrides it (string conversion dispatches statically)
self::$x = require 'f.php'; (and static::/Named::, and [] appends) binds the include to a hidden local the resolver can expand, then stores it in the property. ??=, compound operators and dimension targets with an include value fail at compile time with a clear diagnostic instead of 'Unexpected token: Require'.
…tions preg_quote, strspn, strcspn, strrchr, strip_tags, substr_compare, get_debug_type, http_build_query, version_compare, random_bytes, pack and unpack, as PHP functions in a pay-for-use prelude (src/stdlib_prelude), the PHP_QUERY_RFC1738/3986 constants, and a foldable function_exists() for these functions (guarded vendor polyfills).
__rt_mixed_array_get only understands containers and ArrayAccess objects, so $mixed[$i] on a boxed string (e.g. f()[0] with f(): mixed, monolog's $client::VERSION[0]) silently answered an empty string. Boxed receivers that may hold a string now split at runtime: a string payload is cast, read with StrCharAt (warnings preserved, ?? sees null on a silent miss) and the character persisted before the cast string is released; everything else keeps the boxed container path.
…ble, SessionHandlerInterface, ReflectionType - Every enum implements UnitEnum (backed: BackedEnum); enum interface lists are now transitive (enum implementing B extends A satisfies an A parameter). - Serializable / SessionHandlerInterface checker interfaces (the --web prelude's declaration wins); legacy Serializable without __serialize is rejected instead of silently omitting PHP's Deprecated notice; non-enum classes cannot implement UnitEnum. - natives_prelude: WeakMap (strong keys, documented) and ErrorException (severity; non-null filename/line rejected at compile time), injected post-autoload. - ReflectionType abstract parent of the Reflection type classes. - Catalog entries for all of the above.
…for now - a mixed receiver omitting the argument found no dispatch candidate and threw a catchable Error, so making a parent-required parameter optional is refused - by-reference parameters follow the identical-representation rule - class-relative defaults (self::/static::/parent::) count as changed defaults
'static $a = 1, $b = 2;' (symfony/polyfill-uuid Uuid.php:432) declares one StaticVar per declarator instead of failing with "Expected ';'".
$obj::staticMethod() desugared to call_user_func([$obj, 'm']), whose runtime callable-array dispatch only finds instance methods, so it died with 'callable array did not resolve to an invokable target'. A literal method name now desugars to __elephc_dynamic_static_call($recv, 'm', ...$args), expanded in ir_lower like $class::X: one runtime class-name match per class declaring m, each arm lowering C::m(...) (late static binding intact) or, for an instance method, $this->m(...) when $this is a C and PHP's 'cannot be called statically' Error otherwise. Unknown methods throw 'Call to undefined method'. A candidate with by-reference parameters is refused at compile time. Reachability keeps the method; --strict-php accepts the desugar. $cls::$m() keeps call_user_func.
Prelude injected after the autoload, http_build_query, substr_compare, random_bytes without fallback, get_debug_type, strrchr, version_compare, preg_quote and strip_tags.
…, cache groups Follow-ups to the override-family call ABI: - A body whose declared int/float/string/bool parameter or return was boxed into the family's Mixed ABI re-applies the declared coercion itself: on entry for parameters (int with PHP's TypeError, reusing ReturnBoundaryMixedToInt with an "Argument #n ... given" message) and before boxing its result. The declared-float refusal is gone. - A family whose members differ in arity, by-reference or variadic slots is refused when its ABIs diverge, instead of being skipped. - Checker: an inferred object return is never widened, so callers are not typed more strictly than before; placeholders take the family fold. - Family groups are computed once; widening starts before function and method inference and is dropped once the bodies agree again.
…enum marker checks) - ErrorException is injected only when named; parent::/static calls register the builtin Throwable constructor as reachable (introspection-only subclasses linked against an undefined Exception::__construct). - WeakMap iterates the live insertion order; serialize/unserialize throw like PHP; a null key is the TypeError. - Enums: reject re-implementing UnitEnum/BackedEnum and pure enums reaching BackedEnum; class C implements BackedEnum names BackedEnum. - Plain diagnostic for ErrorException filename/line; redeclared builtin interfaces carry the user's location.
…ing representation - a default spelled self::X/parent::X on both sides is unchanged when both resolve to the same constant value (`handle(..., $type = self::MAIN_REQUEST)`); static:: and a redefined constant still count as changed - a class override whose union-covariant return changes the physical representation of the parent's return (int|null or false under a boxed union, static under static|false) is refused for now; the former codegen test for static|false -> static becomes an error test (it miscompiled through the parent)
…erences - `call()[] = $v` in expression position (e.g. in an arrow fn) is an immediately invoked static closure: an ArrayAccess result gets offsetSet(null, $v), an array result is a discarded temporary, a string throws PHP's Error; the expression yields $v. Appends to a variable or property in expression position are rejected with a clear message. - `new static::$prop(...)`, `new C::$prop`, `new $obj->prop(...)`, `new $a['k']` and their chains build NewDynamic from the class-name reference, as PHP's new_variable grammar does. - `$callback(...)` reports a targeted error: elephc has no runtime Closure::fromCallable, and a wrapper closure would answer reflection differently.
Conflict resolution keeps both sides:
- builtin contract catalogs: union of both sides' entries in catalog
order; registry/support counts add main's fourteen __elephc_bcnumber_*
helpers to this branch's additions.
- ir_lower/function.rs: maybe_undefined frame flags initialize before
main's native Core method bodies and the ordinary body.
- ir_lower/stmt/nested_array_writes.rs: main's rooted write operands with
this branch's early bare-variable read and container acquire.
- ir_lower/expr/{assignments,property_access}.rs: main's WeakMap paths
ahead of this branch's reference-literal and transient-receiver paths.
- checker schema: main's Serializable tentative returns and WeakReference/
unserialize signatures on top of this branch's ReturnTypeWillChange
handling, class-scoped method signatures and enum interface closure.
- pipeline.rs: main's core and bcmath preludes next to the autoloaded
hash prelude.
- unary plus and builtin SPL static-call discovery were added on both
sides; one arm each is kept (unary plus keeps php-src's `expr * 1`).
- generated builtin docs and registries are taken from main here and
regenerated in a follow-up commit.
main now declares WeakMap, WeakReference and ErrorException in
core_prelude, and UnitEnum, BackedEnum and Serializable in
builtin_interfaces. Both sides injecting them made every program that
names WeakMap fail with "Cannot declare class WeakMap" and every
compile fail with "Cannot redeclare built-in interface: UnitEnum".
- Drop natives_prelude (WeakMap, ErrorException) and its catalog marker
interface: main's classes are weak and accept a caller-supplied file
and line, so the filename/line refusal goes with it.
- builtin_native_interfaces keeps SessionHandlerInterface and the
non-enum UnitEnum/BackedEnum check; the legacy Serializable refusal
goes, since main implements the C: envelope.
- main's redeclaration check reports the user's declaration span.
- The enum misuse test expects PHP 8.5's message ("Enum Bad cannot
implement previously implemented interface UnitEnum").
Output of the builtins-docs-sync generators (extract_builtins.py --render --force, gen_module_sections.py, gen_php_comparison.py) after merging origin/main: main's BcMath Number helpers and Core classes/constants next to this branch's internal builtins. No hand edits.
…ip test With main's Core class declarations this program also retains the declared-return coercion helper, whose TypeError messages call get_class() on a concrete object. The test is about main's two eval-bridged Mixed operands, so it counts those; every lookup still has to release its result.
…ses() main's WeakMap prelude declares __ElephcWeakMapIterator as a program statement, so the source-order half of the declared-class list exposed it. Instantiating every declared class (php-src Zend/tests/instantiate_all_classes.phpt) then reached the internal iterator without its map and crashed with SIGSEGV. The source-order names now go through the same internal-name filter as the registered ones.
…sors
Reflecting a user subclass of ReflectionClass (`class Mine extends
ReflectionClass {}` then `new ReflectionClass('Mine')`) exhausted the
default 8 MiB heap.
`new ReflectionClass(<name>)` materializes its metadata graph eagerly and
every ReflectionMethod embeds a declaring-class ReflectionClass. Each of
those nested reflectors of ReflectionClass listed the ~37 private `__*`
backing slots elephc declares on the builtin Reflection classes as
properties and default properties, which put the graph right at the heap
limit. Adding the public `name` property (with its `__propget_name`
accessor) and the `__name_only` slot to ReflectionClass pushed it over.
PHP's Reflection classes declare no private properties, and property-hook
accessors are not PHP methods, so neither should be reflected:
- reflection property lists skip private `__*` slots declared by a builtin
Reflection class (user-declared `__*` properties are still listed);
- method lists (`hasMethod()`, `getMethods()`, the eager `__methods`
metadata) skip `__propget_*` / `__propset_*` accessors, resolved on the
declaring class.
Peak heap for the reproducer drops from 8.49 MB to 5.55 MB. The four
`reflection_subclass_*` var_dump tests run again, and new codegen tests
cover the minimal reproducer and both filters against PHP 8.5 output.
…rties()
`getProperties()` on a receiver known at lowering time (`$rc = new
ReflectionClass('X')` in the same scope, or inline) is expanded statically
by `reflection_class_property_names_for_filter`, which applied no
visibility rule. It still listed the private `__*` storage slots of the
builtin Reflection classes while the runtime metadata path hid them, so
`getProperties()[0]->getName()` returned `__name` instead of `name` for a
ReflectionClass subclass, and it listed a parent's private properties,
which PHP never reports.
Move the rule into `ClassInfo::reflection_lists_property` and use it from
both the codegen metadata (`reflection_property_visible_from_class`) and
the static EIR expansion (list and `getProperties()[N]` index forms), with
one row per name as in the codegen path.
Narrow the docs claim to compiled code: reflection inside eval() fragments
and method_exists() do not apply these filters yet.
main's larger Core class set raised the outer round budget, so the oscillation detector now saw what the smaller budget used to cut off: a CallbackFilterIterator built with a runtime callable array widens the pinned $callback property to an array in the top-level pass, and patch_builtin_spl_storage_signatures pins it back to callable before every method pass. The three spl::decorators::test_callback_filter_iterator_runtime_selected_* tests failed with "outer fixpoint does not converge". Stabilization and the detector now compare the round's classes after the patch, which is what the next round reads; the classes the top level left are kept, as before.
|
Thanks for the batch and for the detailed write-up in #1887. Before reviewing the code, we cross-checked every topic of this branch against main, merged PRs, open issues and open PRs. We also ran representative snippets from your tests on a main nightly, on this branch and on php 8.5.11. The good news: almost every topic fixes behavior that main really gets wrong, and the branch matches php on the probes. But a 49k-line PR cannot be reviewed or merged as one unit. Some parts are already on main, several duplicate open PRs that came first, and the branch fixes about 30 open issues without citing them. We would like it split as described below. Rules for every split PR
1. Already on main: please drop these parts
2. Covered by open PRs that came firstThese PRs predate this branch and implement the same feature. They take precedence: please drop the matching part here, and add any extra behavior on top of them once they land.
The following overlap only at file level, so they mean rebasing, not dropping code: #638, #1528 (moves compilation behind 3. The PRs we would likeIndependent PRs (no open design question)
Design PRs (agreement first, one at a time)These change internal contracts or reverse earlier decisions. Please send each one separately, with its own issue or discussion first, so the design can be reviewed in isolation.
4. The main-only bugs you planned to fileNone of the four has an issue yet, so go ahead. Related issues to link:
Suggested order
This draft can stay open as the reference while the split PRs go up. |
|
Thanks a lot for the thorough cross-check. We followed the split you proposed. The first wave of independent PRs is up. Each one is based on current
We also opened the main-only bugs you mentioned: #1892, #1893, #1894 and #1895, plus the ones the PRs needed (#1896 to #1900). Not in this wave:
Known limitations and open questions are written in each PR body. For example, #1916 asks whether you want php's exact 🤖 Generated with Claude Code |
Refs #1887
Summary
A batch of PHP compatibility fixes, one topic per merged branch, discussed in #1887.
Each topic is a PHP behavior elephc did not support or compiled into something that ran
differently from PHP. Expected outputs come from php 8.5 on the same source, and the
php-src
.phptsuite is the regression net.mainatf96ee9e4a, merged into the branch (no rebase, no force-push).The branch was first based on
fc977b099(v0.27.3);main's 66 new commits came inthrough the merge
20f8a154f, described in "Merge withmain(f96ee9e4a)" below.Merge branch 'fix/<topic>'with its owncommits underneath, so the PR can be reviewed one merge at a time
(
git log --first-parent, then each merge's second parent).throwfix (topic 31), the manifest documentation, the.plansfile, and housekeeping (comment translation, regenerated builtin docs,assembly-comment alignment), then the merge of
mainwith its follow-ups and thereflection fix (8ab9a62, 189575c).
Topics
Commit SHAs are those of the branch. Risk is how likely the topic is to change behavior
outside the pattern it targets.
match (true)arms narrowed by theirinstanceofconditioncodegen::checker_convergefwriteon a non-blocking stream returnsint(0)onEAGAINcodegen::io::file_open_failuresconst, destructuring inwhile, literal-led statements, include into a static property,static $a, $blists, parser recovery)codegen::parser_stmt_forms, parser recovery tests\self/\parent/\staticstay keywordscodegen::parser_names,error_tests::syntax?>,<?=codegen::inline_html,error_tests::inline_html, lexer testsshort_open_tag=offsemantics)codegen::chained_callcodegen::io::file_open_failuresfile_put_contents/fputcsvreturn `intUnitEnum/BackedEnumimplicitly (PHP's marker-interface errors),SessionHandlerInterface,ReflectionTypeas the common parent.WeakMap,ErrorException,Serializableand the enum interfaces themselves now come frommain's Core classes (see the merge section)codegen::native_classes,error_tests::native_classescodegen::oop::override_family_abiinstanceofon templates, hidden marker interfacescodegen::generics*preg_quote,strspn,strcspn,strrchr,strip_tags,substr_compare,get_debug_type,http_build_query,version_compare,random_bytes,pack,unpack)codegen::stdlib_builtins$class::X,$obj::$prop,$obj::m(),newclass-name references,call()[] = $v, string offsets onmixed)codegen::parser_expr,codegen::oop::dynamic_class_memberscodegen::oop::override_compatibility,error_tests::override_compatibilityUnhandledMatchErrorcodegen::control_flow::ternary_unioncodegen::assign_expr,error_tests::assign_exprcodegen::return_coercionf("z");)compact(),extract()codegen::variable_variables,error_tests::variable_variables,parser_tests::variable_variablesReflectionFunctionover themcodegen::callables::fcc_variablecodegen::type_builtins::includes::value_positions,error_tests::io_builtins::includescodegen::reference_placescodegen::dyn_memberfile:line:coldiagnostics, post-autoload preludesautoload_diagnostics_tests,types::checker::driver::testsextract(EXTR_SKIP)respectsuse()capturescodegen::variable_variables,error_tests::variable_variables++self::$x[i],self::${$p}, magic constants in class constants)codegen::static_member,error_tests::static_member&$a[],$a[] = &$x, references to runtime-named propertiescodegen::reference_appendif (! function_exists())polyfills at top level;useinsideyield;->name/->valuethrough enum interfacescodegen::cond_declcodegen::checker_converge,types::checker::latticecodegen::arrays::nested_string_keysvar_dump/print_r/json_encode/serialize/===; readonly never a reference cellcodegen::reference_cyclescodegen::static_ref,error_tests::static_refthrowacceptsmixedand Throwable-or-null unions with the runtime check; throwing an Object-typed null raisesError(explicit extension ofmain's compile-time rule)codegen::exceptions,error_tests::exceptions_enums_magicelephc.includes.toml,--dynamic-includes=report,--include-manifest), documented indocs/compiling/dynamic-includes.mdcodegen::dynamic_includevar_dump/print_r/var_exportvar_dump_object_tests::virtual_hooked_property_is_not_renderedUpstream tests changed
Four upstream tests were adjusted to this branch's semantics:
generics_docblock_methods::test_one_line_block_reads_its_opening_tag(from c769f6c):the method cases now expect PHP's output. This branch drops a method template that no
parameter or return position adopts (
docblock/members.rs), so the method compilesuntyped and runs like php-src (
seven) instead of failing with "does not determine typeparameter ". The function and multi-line assertions are unchanged.
parser_tests::expressions::operators::test_unary_plus_precedence(from 85ac9b9):both sides added unary
+. This branch keeps php-src's lowering,expr * 1(
zend_compile_unary_pm), which keeps-0.0and raises "Unsupported operand types:array * int" like PHP;
mainlowered it to0 + expr. The test now expects the* 1tree; the precedence it checks is the same, andmain's BcMath+$ntests pass.error_tests::core_symbols::test_core_symbols_enum_interface_misuse:enum Bad implements UnitEnumnow expects PHP 8.5's message, "Enum Bad cannot implementpreviously implemented interface UnitEnum", which this branch already reported.
ir_lower::tests::call_coercion_owners::eval_class_name_results_release_bridge_cells_and_strings_on_all_targetscounts only
main's two lookups: with the Core class declarations, the program alsoretains this branch's declared-return coercion helper, whose own
get_class()callsare concrete (not eval-bridged). Every lookup must still release its result.
The earlier adjustment of
serialize::not_serializableis gone:mainrewrote that testitself (namespaced
Demo\WeakMap/Demo\ReflectionType), and its version is kept.Merge with
main(f96ee9e4a)mainmoved 66 commits sincefc977b099. They were merged into the branch with20f8a154f(signed, no rebase), followed by two adaptation commits and the regeneratedbuiltin docs (6101823). The conflicts and how they were resolved:
Builtin contract catalogs (
catalog_{classes,constants,data}.rs,registry.rs,support.rs): union of both sides' entries in catalog order; the registry counts addmain's fourteen__elephc_bcnumber_*helpers to this branch's additions.ir_lower/function.rs: the possibly-undefined frame flags (topic 15) are initializedbefore
main's native Core method bodies (core_methods::lower) and the ordinary body.ir_lower/stmt/nested_array_writes.rs:main's rooted write operands(
root_owned_call_operand/retire_owned_call_operand) with this branch's early readof a bare-variable right-hand side and its container acquire (topic 28).
ir_lower/expr/{assignments,property_access}.rs:main'sWeakMappaths run first,then this branch's reference-literal and transient-receiver paths (topics 20 and 25).
Checker schema (
classes/interfaces.rs,classes/methods.rs,enums.rs):main'stentative
Serializablereturns and itsWeakReference::create/unserializesignaturetweaks, on top of this branch's
#[\ReturnTypeWillChange]handling, class-scoped methodsignatures and enum interface closure.
pipeline.rs:main's Core and BcMath preludes next to this branch'sautoload-aware hash prelude (topic 22).
Same feature on both sides: unary
+(see above) and the builtin SPLStaticMethodCalldiscovery arm. One arm each is kept; the SPL one ismain's, withthis branch's case-insensitive
parent/self/staticmatch.Core classes (e4b3f9d):
mainnow declaresWeakMap,WeakReferenceandErrorException(core_prelude) andUnitEnum,BackedEnumandSerializable(
builtin_interfaces). Both sides declaring them broke every program namingWeakMap("Cannot declare class WeakMap") and every compile ("Cannot redeclare built-in interface:
UnitEnum"). This branch's
natives_preludeis removed in favor ofmain's classes, whichare truly weak and accept a caller-supplied file and line, so the branch's
ErrorExceptionfilename/line refusal and its legacy-Serializablerefusal are gone too.What remains of topic 8 is the enum marker-interface checks,
SessionHandlerInterfaceandReflectionType.main's redeclared-interface error now points at the user'sdeclaration.
Internal Core prelude classes (5b263e3):
main's__ElephcWeakMapIteratoris aprogram-level declaration, so
get_declared_classes()listed it and php-src'sinstantiate_all_classes.phptcrashed instantiating it. The declared-class list nowfilters internal names on both halves.
Checker outer rounds (f287126):
main's Core classes raise the outer round budget,so the branch's oscillation detector (topic 27) now saw an alternation the smaller budget
used to cut off: a
CallbackFilterIteratorover a runtime callable array widens thepinned
$callbackproperty, andpatch_builtin_spl_storage_signaturespins it backbefore every method pass. Rounds are now compared after that patch, which is what the
next round reads; the five
spl::{decorators,recursive}::*runtime_selected*tests pass.Reflection fix (8ab9a62, 189575c)
new ReflectionClass('Mine')withclass Mine extends ReflectionClassexhausted the 8 MiBheap. Reflection metadata is built eagerly, and the nested reflectors of a builtin
Reflection class listed its ~37 private backing slots and the synthetic
__propget_*accessors; the public
nameproperty this branch adds pushedmain's already tight graphover the limit. Property lists now skip the private
__*slots a builtin Reflection classdeclares (user
__*properties are still listed), and method lists skip the__propget_*/__propset_*accessors, as PHP shows neither. The same listing rule isshared with static
getProperties(). The reproducer's peak heap drops from 8.49 MB to5.55 MB and the four
var_dump_object_tests::reflection_subclass_*tests run again. Theeager design itself is still filed as a separate issue (item 4 below).
Bugs that reproduce on
mainaloneFound while merging
main; they reproduce withfc977b099alone, so they go as separateissues (drafts ready):
is_object()does not narrow, so with(array)cast fails EIR lowering, and the checker types it inconsistently (Array(Int) vs Array(Mixed)) #707's(array)typing the validif (is_object($d)) { $i = (array) $d; } else { $i = $d; }overarray|objectfailswith "cannot reassign".
intplaceholder, so
array_map(strtoupper(...), (array) $p)fails with "callback parameter$s expects string, got int" (before
(array)cast fails EIR lowering, and the checker types it inconsistently (Array(Int) vs Array(Mixed)) #707 the cast wasarray<mixed>).tests/lang/foreachLoop.013(by-referenceforeachplusarray_unshiftinsideeval)crashes with SIGSEGV, and
Zend/tests/gc/gc_049exhausts the heap.new ReflectionClass('Mine')overclass Mine extends ReflectionClass, or reflectingclass X extends ArrayObject,exhaust the 8 MiB default heap.
Laravel compatibility
The motivation is general PHP compatibility, but several of these fixes were found by
compiling Laravel 13 and Symfony vendor code, used as a stress corpus for the parser and
the checker:
function_exists()polyfill folding (topic 26): theif (! function_exists('f'))idiom of
helpers.phpfiles.Filesystem::getRequire, config/route/event caches,CarbonPeriod'sversion_comparecondition.oscillate (
Request::is(...$patterns),CarbonInterval::__callStatic,HttpResponseException::$response) and the checker spent most of its time in quadraticscans.
EventDispatcher::addListener($this->listeners[$eventName][$priority][] = $listener).value()/tap()/with()widening (topic 27): untyped helper parameters thatreceive closures and scalars, and
callable|nulldefaults.DebugClassLoader(
self::$darwinCache[$dir] = &self::$darwinCache[$k]) andLocaleFallbackProvider(
static $p; $p ??= require F;).$this['env'] = vinApplication::detectEnvironment(topic 15),defer()'scall()[] = $v(topic 12),laravel/promptskeyword namespaces (topic 4), Carbon's$instance::setLastErrors()(topic 12), Guzzle/Symfony polyfill statement forms(topic 3),
enum_value()over joined enums (topic 1), the serializable-closureNative::mapPointersreferences (topic 25).Current state:
--checkover a Laravel 13 application converges in 2 outer rounds and7 method passes (≈45 s, 3.7 GB RSS) and reports 14 652 remaining errors. By family:
CurlFactory)How it was validated
.phptsuite (10 009 tests, php 8.5.10 as reference, run-tests' comparator):1752 → 2120 passing tests,
main(f96ee9e4a) vs this branch after the merge.Before the merge the branch had 2109 (
mainatfc977b099: 1726). Against that run themerge gains 16 tests and loses 5, none of them from this branch's code:
attributes/override/properties_19andgh18833fail the same way onmainalone;inheritance/bug71248now reaches aneval()failure thatmainalone also hits(an eval'd subclass that changes an inherited constructor's signature);
instantiate_all_classescrashed onmain's internal WeakMap iterator and is fixed by5b263e3;
file/bug75679is a path-length artefact of the run directory and passeson its own.
main(
fc977b099), 418 on the branch, unchanged by the merge. The 6 that fail aredocumented limitations.
for the parser and the checker.
How to test
Local results on macOS aarch64 after the merge and the reflection fix: error_tests 1934,
parser_tests 498, lexer_tests 234, lib 5248 (0 failed, 18 ignored), var_dump_object_tests
64 (0 ignored), and the codegen suites of every touched area green (reflection,
oop::,class, Core classes, BcMath Number, nested writes, serialize). Builtins docs regenerate
with no drift, and the curl-feature contract and image-stub checks of CI pass.
AI assistance
This work was written with substantial help from Claude Code (Anthropic). Each topic went
through review rounds against php 8.5 output, the php-src
.phptsuite and the probefamilies above, and I reviewed the changes and remain responsible for them.
🤖 Generated with Claude Code