Commit d4beb94
committed
Round-trip closures declared in constant expressions
PHP 8.5 allows anonymous closures in constant expressions: attribute
arguments, class constants, property and parameter defaults, property
hooks. They are compile-time checked to be static and capture-free, so
they carry no state and are fully described by their declaration site.
deepclone_to_array() now encodes them under a new mask marker (LONG 1)
as [class, site, attrIndex|null, closureIndex, startLine], where site
is "" (the class), "NAME" (constant or enum case), "$name" (property),
"name()" (method), "name()#N" (parameter) or "$name::get()" /
"$name::set()#N" (property hooks).
Identification is exact: a const-expr closure shares its opcodes with
the op_array embedded in the declaring AST, so the scope class's sites
are scanned in declaration order for a pointer match. Promoted
properties are addressed through their constructor parameter, the
canonical surface. deepclone_from_array() re-evaluates the addressed
constant expression, picks the Nth closure from a depth-first walk
(arrays in order, objects through their array-cast properties) and
verifies the declaration line still matches, so payloads that outlived
a code change fail loudly ("stale payload") instead of resolving to a
moved closure.
The payload carries no code, only names and indices; the resolved code
is whatever the loaded class declares. allowed_classes gates both
directions: "Closure" must be allowed before any constant expression is
evaluated on the to_array side, and the payload-named class must be
allow-listed before it is autoloaded on the from_array side, keeping
unserialization free of payload-driven constructor execution.
Closures created at runtime (and any closure the scanner cannot match)
keep throwing NotInstantiableException as before.1 parent f0c7f81 commit d4beb94
3 files changed
Lines changed: 986 additions & 17 deletions
0 commit comments