Skip to content

Reference const-expr closures through engine ids on PHP 8.6#24

Open
nicolas-grekas wants to merge 1 commit into
mainfrom
native-constexpr-ids
Open

Reference const-expr closures through engine ids on PHP 8.6#24
nicolas-grekas wants to merge 1 commit into
mainfrom
native-constexpr-ids

Conversation

@nicolas-grekas

@nicolas-grekas nicolas-grekas commented Jun 10, 2026

Copy link
Copy Markdown
Member

The proposed PHP 8.6 "Serializable closures" engine support (implementation at nicolas-grekas/php-src#4) gives every closure declared in an attribute argument or in a parameter default value a canonical per-class id, derived from a deterministic, non-evaluating walk over the class's constant expressions, exposed as ReflectionFunction::getConstExprId() and resolved by Closure::fromConstExpr(). This PR makes deepclone use those ids, gated on PHP_VERSION_ID >= 80600, assuming the RFC lands.

  • Encoding. On PHP 8.6, deepclone_to_array() calls the exported zend_constexpr_closure_ref() and emits [class, id, line] under the existing mask marker. This replaces the per-call declaration-site scan, which had to evaluate every preceding site to count closures: on a 60-site class, encoding the last closure drops from 8.3 us to 1.2 us, and resolving it from 0.3 us scan-equivalents to a 17 ns/site pointer walk. Closures declared in class constant values and in property default values are evaluated in place by the engine and have no id; they keep the site-based 5-element form.
  • Decoding. deepclone_from_array() accepts both forms on every PHP version, discriminated by the type of element 1 (int id vs string site). Site-based payloads written on PHP 8.5 keep resolving on PHP 8.6, so caches survive the upgrade; engine-id payloads on older PHP fail with a message saying they need PHP 8.6. Resolution goes through zend_constexpr_closure_site_by_id() plus the same staleness check as before (stale payload when the declaration line moved). Crafted payloads addressing a first-class-callable site are rejected; FCCs keep the named-closure form.
  • Gating is unchanged. Closure must be allowed before anything is evaluated on the to_array side, and the payload-named class is allow-list-checked before zend_lookup_class() on the from_array side.
  • Runtime closures now surface the engine's own Closure::__serialize() refusal (Serialization of 'Closure' is not allowed) instead of NotInstantiableException, since Closure gains that method on 8.6.

The new code only compiles on PHP >= 8.6, so the current CI matrix is unaffected; the 8.6 paths were validated locally against the patched engine (all phpts pass, and the polyfill suite passes against this build with byte-identical, cross-resolvable payloads). deepclone_constexpr_closures_native.phpt covers the 8.6 behavior, deepclone_constexpr_closures_id_pre86.phpt the refusal on older PHP.

Companion polyfill implementation: symfony/polyfill#633

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.

1 participant