Context
Work on #455 exposed a code-size and compile-time scaling problem around dynamic PHP callables.
A callback stored as mixed can resolve at runtime to a function-name string, closure/first-class descriptor, invokable object, instance method array, or static method array. The current backend therefore emits selection code that inspects the runtime shape and routes the call to a matching descriptor.
The optimization developed alongside #455 shares module-wide callable wrappers, invokers, and immutable descriptor templates. This removes the largest duplicated bodies. However, each dynamic call site still emits its own:
- runtime tag switch;
- per-candidate string-name comparison loop;
- class-ID selection loops for invokable objects and method arrays;
- argument-container setup and descriptor invocation glue.
For N equivalent dynamic call sites and M callable candidates, the large wrapper/invoker bodies are now approximately O(M), but meaningful selector assembly remains approximately O(N × M).
This is especially visible in compatibility adapters such as the legacy callable overload of session_set_save_handler(), but it is a global call_user_func() / dynamic-call concern rather than a web-only problem.
Goal
Explore a module-wide or runtime-wide callable dispatcher so equivalent dynamic call sites can invoke one shared selector instead of embedding the full candidate-selection loop at every source location.
The common static/direct-call paths must remain direct and should not pay for dynamic dispatch.
Design questions
Potential approaches to evaluate:
-
Generated module-level dispatcher per argument/signature shape
- Emit the tag switch and candidate loops once per compatible ABI shape.
- Call sites only materialize the callback/arguments and call the shared dispatcher.
-
Table-driven runtime lookup
- Publish module callable metadata in a compact table.
- Resolve function names through a hash/index instead of a linear emitted comparison chain.
- Keep class/method and receiver-capture metadata target-independent where possible.
-
Hybrid
- Generated per-shape entry points backed by shared runtime lookup helpers.
- Preserve specialized handling where by-reference parameters, receiver captures, or return shapes require it.
Questions that need explicit answers:
- What is the right cache/dispatcher key: visible argument shape, full
FunctionSig, boxed argument container, or another ABI-level representation?
- How should by-reference parameters and writebacks be represented?
- How do named/spread argument semantics interact with a shared runtime entry?
- Which descriptor pieces can remain immutable and shared when an instance receiver must be captured per call?
- How should undefined-function and non-callable diagnostics retain the correct PHP-visible operation name?
- Is a hash table worthwhile for small candidate sets, or should lookup strategy vary by table size?
- How do we avoid linking optional callable/runtime support into programs that never use dynamic callables?
Required semantics
The design must preserve all supported PHP callable forms:
- function-name strings;
- closures and first-class callables;
- invokable objects;
[$object, 'method'];
['Class', 'method'] and 'Class::method';
- malformed/non-callable fatal behavior;
- argument evaluation order, return representation, ownership, and by-reference writebacks.
It must support every target in the supported matrix: macOS AArch64, Linux AArch64, and Linux x86_64.
Suggested benchmark
Add a focused generator/fixture with 1, 10, 100, and 1,000 equivalent dynamic call sites, measured separately for:
- compile time;
- codegen time;
- assembly bytes and line count;
- number of callable selector/invoker labels;
- final binary size;
- runtime throughput for repeated invocation.
Include at least:
- runtime function-name strings;
- boxed
mixed callbacks;
- invokable objects;
- instance/static method arrays;
- more than one argument/signature shape.
The benchmark must distinguish one call site executed 1,000 times from 1,000 distinct source call sites.
Acceptance criteria
Non-goals
- Removing
call_user_func().
- Dropping the deprecated-but-supported procedural
session_set_save_handler() overload.
- Replacing direct calls or statically resolved first-class callables with runtime dispatch.
Context
Work on #455 exposed a code-size and compile-time scaling problem around dynamic PHP callables.
A callback stored as
mixedcan resolve at runtime to a function-name string, closure/first-class descriptor, invokable object, instance method array, or static method array. The current backend therefore emits selection code that inspects the runtime shape and routes the call to a matching descriptor.The optimization developed alongside #455 shares module-wide callable wrappers, invokers, and immutable descriptor templates. This removes the largest duplicated bodies. However, each dynamic call site still emits its own:
For
Nequivalent dynamic call sites andMcallable candidates, the large wrapper/invoker bodies are now approximatelyO(M), but meaningful selector assembly remains approximatelyO(N × M).This is especially visible in compatibility adapters such as the legacy callable overload of
session_set_save_handler(), but it is a globalcall_user_func()/ dynamic-call concern rather than a web-only problem.Goal
Explore a module-wide or runtime-wide callable dispatcher so equivalent dynamic call sites can invoke one shared selector instead of embedding the full candidate-selection loop at every source location.
The common static/direct-call paths must remain direct and should not pay for dynamic dispatch.
Design questions
Potential approaches to evaluate:
Generated module-level dispatcher per argument/signature shape
Table-driven runtime lookup
Hybrid
Questions that need explicit answers:
FunctionSig, boxed argument container, or another ABI-level representation?Required semantics
The design must preserve all supported PHP callable forms:
[$object, 'method'];['Class', 'method']and'Class::method';It must support every target in the supported matrix: macOS AArch64, Linux AArch64, and Linux x86_64.
Suggested benchmark
Add a focused generator/fixture with 1, 10, 100, and 1,000 equivalent dynamic call sites, measured separately for:
Include at least:
mixedcallbacks;The benchmark must distinguish one call site executed 1,000 times from 1,000 distinct source call sites.
Acceptance criteria
Non-goals
call_user_func().session_set_save_handler()overload.