Skip to content

Conversation

@arnaud-lb
Copy link
Owner

@arnaud-lb arnaud-lb commented Sep 8, 2025

PFAs are created by generating the AST of a Closure at runtime and compiling it. The compiled op_array can be cached similarly to linked classes.

About caching

There are two levels of caching:

  • The global opcache (ZCG(hash))
  • The caller's run time cache

The cache key in ZCG(hash) is composed of the address of the declaring opline, the address of the function being called, and a prefix.

This scheme is not ideal because:

  • The key used in ZCG(hash) may be a valid file name
  • This takes a slot in ZCG(hash)
  • When opcache is not enabled, only the run time cache is used. This cache stores only one entry, so cache misses will occur when the function being called changes.

A better solution might be to cache PFA op_arrays in the declaring op_array (similarly, the inheritance cache is stored in classes). This cache could store multiple PFAs, and could be used regardless of opcache being enabled.

Alternatively, use a global two-level map: First level is indexed by the "signature" of the PFA, second level by the function being called. This would lead to a better cache hit rate when the same function is partialled in multiple locations.

TODO

  • Improve PFA caching when opcache is not enabled, and fix cases that are broken when opcache is not enabled
  • Reorganize tests
    • Update RFC-example tests
  • PFAs in constant expressions
  • bindTo() is a no-op for PFA of closures
  • assert(), compact(), extract(), func_get_arg(), get_defined_vars()
  • Pre-bound literal optimization

@arnaud-lb arnaud-lb force-pushed the partials-v2-gen branch 3 times, most recently from 7062e66 to f65bf45 Compare September 13, 2025 10:37
@arnaud-lb arnaud-lb force-pushed the partials-v2-gen branch 2 times, most recently from d820a4f to 75dbd22 Compare November 3, 2025 17:04
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get a test case where the function has optional arguments and an incorrect number is given (just to see the error message once).

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, I just realize that the error messages for userland and native functions are already inconsistent with each other:

https://3v4l.org/3G8EO

try {
test(1,...)(?);
} catch (Error $ex) {
echo "OK";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
echo "OK";
echo $ex::class, ": ", $ex->getMessage(), PHP_EOL;

Comment on lines +1680 to +1682
if (UNEXPECTED(zend_call_trampoline_arginfo[0].name == NULL)) {
zend_call_trampoline_arginfo[0].name = ZSTR_KNOWN(ZEND_STR_ARGS);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to do this at MINIT, so that it's not necessary to check the (unexpected) path for every call?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree

Comment on lines +1236 to +1241
ZEND_API bool zend_check_type_ex(
const zend_type *type, zval *arg, zend_class_entry *scope,
bool is_return_type, bool is_internal)
{
return zend_check_type(type, arg, scope, is_return_type, is_internal);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of this? Wouldn't it work to expose zend_check_type() directly?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not that easy, as the function is always_inline, and uses always_inline functions from this file as well. zend_check_type() is critical, I didn't want to take the risk of performance regression.

So we have the following options:

  • Move the function and a bunch of functions it uses to a header file (zend_check_type, zend_check_type_slow, zend_check_intersection_type_from_list, probably others), so we don't lose inline-ability
  • Make the function extern inline, but we don't do that (there was a thread about that in a PR).
  • Export a wrapper, like I did here

I will think about about the other options

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.

3 participants