feat: Implement extension methods (RFC prototype, phase 1)#22635
Draft
hollyschilling wants to merge 5 commits into
Draft
feat: Implement extension methods (RFC prototype, phase 1)#22635hollyschilling wants to merge 5 commits into
hollyschilling wants to merge 5 commits into
Conversation
Adds Swift-style extension blocks that declare additional methods for an
existing class or interface, resolved at call time only when the receiver
does not define the method itself:
extension Target {
public function helper(): string { /* $this is the receiver */ }
}
- `extension` is a contextual keyword (T_EXTENSION), lexed only when
followed by another identifier, mirroring the enum technique; a
`extension extends/implements` carve-out preserves classes named
"extension", and T_EXTENSION is semi-reserved.
- Extension blocks compile to a synthetic final anonymous class; a new
ZEND_BIND_EXTENSION opcode (212) registers its methods, keyed by the
lowercased resolved target name, when the declaration executes
(load-gated, like function declarations).
- zend_std_get_method gains one fallback on its miss path: real method,
then __call trampoline, then extension registry lookup (most-derived
target first: inheritance chain, then interfaces). Registered methods
are flagged ZEND_ACC_NEVER_CACHE so the polymorphic inline cache and
JIT are bypassed in this prototype.
- Methods only: properties, constants, and cases are compile errors.
- Indirect invocation (callables, reflection, method_exists) deliberately
does not see extensions.
PROTOTYPE LIMITATIONS: the registry is a process global, so user-class
method pointers dangle after request 1 in multi-request SAPIs (CLI only
until it moves into zend_executor_globals); duplicate registrations are
silently ignored (first wins).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zend_extension_methods_register() set ZEND_ACC_NEVER_CACHE on the synthetic class's methods at bind time. Under opcache the class image lives in shared memory and is immutable at runtime; with opcache.protect_memory=1 (as CI runs) the write faults (SIGBUS on macOS, Termsig=10 in run-tests output). Apply the flag at compile time instead, before the class is persisted: zend_compile_class_decl() now returns the compiled zend_class_entry (all existing callers ignore the return value), and zend_compile_extension_decl() flags the methods on the returned CE. Registration no longer writes to the functions at all. Verified with opcache.enable_cli=1 + opcache.protect_memory=1 and with the tracing JIT; full Zend suite green under both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The trace recorder handles ZEND_ACC_NEVER_CACHE callees by recording the INIT with an unknown (NULL) function, but until now every such function was trampoline-like, so the recorder could never follow into its body. Extension methods are never-cached functions with ordinary, traceable bodies: the recorder logged a NULL-func call frame, then recorded ENTER into the body, and zend_jit_trace_build_tssa asserted (&call->func->op_array == op_array) on the contradiction. Stop the trace when entering a never-cached function's body, matching the existing trampoline/property-hook/fake-closure treatment. Calls to extension methods stay interpreted, as intended for this prototype. Reproduces with run-tests' JIT settings (opcache.jit_hot_func=1 et al). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zend_extension_methods.c was added to configure.ac but not to the Windows source list in win32/build/config.w32, so the Windows build failed at link with unresolved externals. Also regenerate ext/tokenizer data (tokenizer_data_gen.php + gen_stub) so the T_EXTENSION constant and token_name() know the new token. Verified locally on a ZTS debug build (--enable-zts) with opcache protect_memory and the tracing JIT; Zend + tokenizer suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Magic methods dispatch through dedicated class-entry slots and object handlers (ce->constructor, ce->__call, the cast handler, ...), never through the zend_std_get_method miss path where extension methods resolve. An extension-declared magic method could therefore never work as a magic method - at best dead code, at worst reachable only through a direct $obj->__construct() style call. Reject the reserved __ prefix wholesale at compile time, mirroring the enum restriction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
b1a7ca8 to
8bbf4af
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds Swift-style extension blocks that declare additional methods for an existing class or interface, resolved at call time only when the receiver does not define the method itself:
extensionis a contextual keyword (T_EXTENSION), lexed only when followed by another identifier, mirroring the enum technique; aextension extends/implementscarve-out preserves classes named "extension", and T_EXTENSION is semi-reserved.PROTOTYPE LIMITATIONS: the registry is a process global, so user-class method pointers dangle after request 1 in multi-request SAPIs (CLI only until it moves into zend_executor_globals); duplicate registrations are silently ignored (first wins).