Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ PHP NEWS
?? ??? ????, PHP 8.6.0alpha1

- Core:
. Added generics: type parameters on classes, interfaces, traits, functions,
methods, closures, and arrow functions, with optional bounds, defaults,
and variance markers; turbofish syntax (`f::<int>()`) at call sites; and
type arguments on named types (`Box<int>`, `array<K, V>`, `iterable<T>`,
`self<T>`, `static<T>`, `parent<T>`). Type parameters erase to their bound
at runtime; type arguments are discarded. Pre-erasure metadata is preserved
for Reflection so static-analysis tools can consume generics without
re-parsing source. (azjezz)
. Added first-class callable cache to share instances for the duration of the
request. (ilutov)
. It is now possible to use reference assign on WeakMap without the key
Expand Down
51 changes: 51 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,31 @@ PHP 8.6 UPGRADE NOTES
========================================

- Core:
. Added support for runtime-bound-checked generics. Classes, interfaces,
traits, functions, methods, closures, and arrow functions can now declare
type parameters with optional bounds (`T : Foo`), defaults (`T = int`),
and variance markers (`+T`, `-T`):

class Box<T : object> {
public T $value;
public function get(): T { return $this->value; }
}

function id<T>(T $x): T { return $x; }

Call sites accept turbofish type arguments (`Box::<int>::new()`,
`id::<int>(7)`); use sites accept type arguments on named types
(`Box<int>`, `array<int, string>`, `iterable<T>`, `self<T>`,
`static<T>`, `parent<T>`). Recursive bounds (`T : Comparable<T>`)
are supported. Anonymous classes cannot declare type parameters.

At runtime each type parameter is replaced by its declared bound
(or `mixed` when unbounded, or when the bound is invalid in the
target position, e.g. `callable` on a property), and type
arguments are discarded. Pre-erasure metadata is preserved on
functions, methods, and class entries and is exposed through
Reflection so that PHP-based static-analysis tools can consume
generics without re-parsing source.
. It is now possible to use reference assign on WeakMap without the key
needing to be present beforehand.

Expand Down Expand Up @@ -236,6 +261,23 @@ PHP 8.6 UPGRADE NOTES
RFC: https://wiki.php.net/rfc/isreadable-iswriteable
. Added ReflectionParameter::getDocComment().
RFC: https://wiki.php.net/rfc/parameter-doccomments
. Added ReflectionFunctionAbstract::isGeneric() and
ReflectionFunctionAbstract::getGenericParameters() (covers
ReflectionFunction, ReflectionMethod, closures, and arrow functions).
. Added ReflectionClass::isGeneric() and
ReflectionClass::getGenericParameters().
. Added ReflectionClass::getGenericArgumentsForParentClass(),
ReflectionClass::getGenericArgumentsForParentInterface(string $name),
and ReflectionClass::getGenericArgumentsForUsedTrait(string $name) for
inspecting the type arguments supplied at a class's own extends /
implements / use sites. Returns null when no type arguments were
specified for that ancestor at this class's clause site (consumers
enumerate ancestors via the existing getParentClass() / getInterfaces()
/ getTraits() APIs).
. Added ReflectionNamedType::hasGenericArguments() and
ReflectionNamedType::getGenericArguments(). The arguments are returned
as ReflectionType instances in source order (pre-erasure form);
ReflectionNamedType::getName() continues to return the erased name.

- Intl:
. `grapheme_strrev()` returns strrev for grapheme cluster unit.
Expand All @@ -262,6 +304,15 @@ PHP 8.6 UPGRADE NOTES
. Openssl\Session
RFC: https://wiki.php.net/rfc/tls_session_resumption

- Reflection:
. ReflectionGenericTypeParameter (final, instances obtained via
ReflectionClass::getGenericParameters() and
ReflectionFunctionAbstract::getGenericParameters()).
. ReflectionTypeParameterReference (extends ReflectionType, appears only
inside pre-erasure type expressions: bounds, defaults, and the elements
of ReflectionNamedType::getGenericArguments()).
. enum ReflectionGenericVariance { Invariant; Covariant; Contravariant }.

- Standard:
. enum SortDirection
RFC: https://wiki.php.net/rfc/sort_direction_enum
Expand Down
48 changes: 48 additions & 0 deletions UPGRADING.INTERNALS
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,54 @@ PHP 8.6 INTERNALS UPGRADE NOTES
zend_fci_consumed_arg(), which allows moving a selected callback argument
instead of copying it in zend_call_function(). Currently only a single
consumed argument is supported.
. Added support for runtime-bound-checked generic type parameters. The
main additions:
. New types in zend_compile.h: zend_generic_parameter,
zend_generic_parameter_list, zend_generic_type_table, and
zend_generic_scope_entry. Allocate / destroy via
zend_generic_parameter_list_alloc(),
zend_generic_parameter_list_destroy(),
zend_generic_type_table_alloc(), and
zend_generic_type_table_destroy().
. zend_op_array and zend_class_entry both gained an optional
`generic_parameters` (declared parameter list) and an optional
`generic_types` side table holding the pre-erasure forms of
return types, parameter types, property types, class-constant
types, the extends type, implements list, and trait-use list.
The runtime arg_info / property / class-constant slots continue
to hold only the erased form.
. New AST kinds: ZEND_AST_GENERIC_TYPE_PARAMETER_LIST,
ZEND_AST_GENERIC_TYPE_PARAMETER, ZEND_AST_GENERIC_NAMED_TYPE,
ZEND_AST_GENERIC_TYPE_ARGUMENT_LIST, ZEND_AST_TURBOFISH.
. zend_ast_decl::child[] grew from 5 to 6 entries; the new slot
carries an optional generic-parameter-list AST.
. The child-count groups of ZEND_AST_CALL, ZEND_AST_NEW,
ZEND_AST_METHOD_CALL, ZEND_AST_NULLSAFE_METHOD_CALL, and
ZEND_AST_STATIC_CALL each gained one optional child holding the
call-site turbofish type-argument list. Code that walks these
nodes by hard-coded child count must be updated.
. zend_ast_export handles the new generic AST kinds.
. Two new bits on zend_type's type_mask:
_ZEND_TYPE_TYPE_PARAMETER_BIT (1u << 25) and
_ZEND_TYPE_NAMED_WITH_ARGS_BIT (1u << 31), with payload structs
zend_type_parameter_ref { zend_string *name; uint32_t index;
uint8_t origin; } and zend_type_named_with_args { zend_string
*name; uint32_t name_attr; uint32_t count; zend_type args[]; }.
These bits only ever appear in pre-erasure forms held by the
side table; runtime arg_info / property / class-constant types
never carry them. Helpers: ZEND_TYPE_HAS_TYPE_PARAMETER(),
ZEND_TYPE_TYPE_PARAMETER(), ZEND_TYPE_HAS_NAMED_WITH_ARGS(),
ZEND_TYPE_NAMED_WITH_ARGS().
. New compiler-globals fields: CG(type_arg_depth) (right-angle
split state used by the zendlex wrapper), CG(token_residual)
(single-token pushback slot), and CG(generic_scope) (linked
stack of in-scope type parameters).
. New T_TURBOFISH lexer token (literal `::<`). The zendlex wrapper
splits T_SR (`>>`), T_IS_GREATER_OR_EQUAL (`>=`), and T_SR_EQUAL
(`>>=`) into separate `>` tokens whenever CG(type_arg_depth) is
non-zero, with a single-token pushback slot.
. Module API bumped to 20260506; extension API bumped to
420260506. All extensions must be recompiled.

========================
2. Build system changes
Expand Down
45 changes: 45 additions & 0 deletions Zend/Optimizer/optimize_func_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,46 @@ static void zend_delete_call_instructions(const zend_op_array *op_array, zend_op
}
}

/* Returns true if a VERIFY_GENERIC_ARGUMENTS sits between this call's INIT and
* DO opcodes; such a call cannot be inlined because the verify opcode reads
* EX(call), which goes away once the frame is dropped. */
static bool zend_call_has_generic_arguments_check(zend_op *opline)
{
int call = 0;
while (1) {
switch (opline->opcode) {
case ZEND_INIT_FCALL_BY_NAME:
case ZEND_INIT_NS_FCALL_BY_NAME:
case ZEND_INIT_STATIC_METHOD_CALL:
case ZEND_INIT_METHOD_CALL:
case ZEND_INIT_FCALL:
case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL:
if (call == 0) {
return false;
}
ZEND_FALLTHROUGH;
case ZEND_NEW:
case ZEND_INIT_DYNAMIC_CALL:
case ZEND_INIT_USER_CALL:
call--;
break;
case ZEND_DO_FCALL:
case ZEND_DO_ICALL:
case ZEND_DO_UCALL:
case ZEND_DO_FCALL_BY_NAME:
call++;
break;
case ZEND_VERIFY_GENERIC_ARGUMENTS:
if (call == 0) {
return true;
}
break;
}

opline--;
}
}

static void zend_try_inline_call(zend_op_array *op_array, const zend_op *fcall, zend_op *opline, const zend_function *func)
{
const uint32_t no_discard = RETURN_VALUE_USED(opline) ? 0 : ZEND_ACC_NODISCARD;
Expand All @@ -97,6 +137,11 @@ static void zend_try_inline_call(zend_op_array *op_array, const zend_op *fcall,
return;
}

if (zend_call_has_generic_arguments_check(opline - 1)) {
/* The verify opcode must run; inlining would orphan it. */
return;
}
Comment on lines +140 to +143
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

zend_try_inline_call inlines simple function calls by replacing their INIT_FCALL and DO_FCALL opcodes with NOP.

e.g. before the optimizer:

INIT_FCALL                ; allocates the call frame
VERIFY_GENERIC_ARGUMENTS  ; reads EX(call)->func
DO_FCALL

after inlining without the guard:

NOP                       ; frame never set up
VERIFY_GENERIC_ARGUMENTS  ; EX(call)->func walks an unrelated frame causing segfault
NOP

The constraint is permanent: any future pass that elides an INIT_FCALL/DO_FCALL pair has to consider whether intervening opcodes depend on the call frame.


for (i = 0; i < num_args; i++) {
/* Don't inline functions with by-reference arguments. This would require
* correct handling of INDIRECT arguments. */
Expand Down
33 changes: 33 additions & 0 deletions Zend/Optimizer/zend_optimizer.c
Original file line number Diff line number Diff line change
Expand Up @@ -1734,12 +1734,45 @@ ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_l
uint32_t fn_flags2 = op_array->fn_flags2;
zend_function *prototype = op_array->prototype;
HashTable *ht = op_array->static_variables;
zend_arg_info *arg_info = op_array->arg_info;
bool arg_info_substituted = (arg_info != orig_op_array->arg_info);

*op_array = *orig_op_array;
op_array->fn_flags = fn_flags;
op_array->fn_flags2 = fn_flags2;
op_array->prototype = prototype;
op_array->static_variables = ht;
if (arg_info_substituted) {
op_array->arg_info = arg_info;
}
}
}
} ZEND_HASH_FOREACH_END();

zend_property_info *prop;
ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&ce->properties_info, name, prop) {
if (!(prop->flags & ZEND_ACC_GENERIC_CLONE) || !prop->hooks) {
continue;
}

const zend_property_info *parent_prop = zend_hash_find_ptr(&prop->ce->properties_info, name);
if (!parent_prop || !parent_prop->hooks) {
continue;
}

for (uint32_t hi = 0; hi < ZEND_PROPERTY_HOOK_COUNT; hi++) {
zend_function *clone_hook = prop->hooks[hi];
zend_function *parent_hook = parent_prop->hooks[hi];
if (!clone_hook || !parent_hook || clone_hook == parent_hook) {
continue;
}

zend_arg_info *arg_info = clone_hook->op_array.arg_info;
bool arg_info_substituted = (arg_info != parent_hook->op_array.arg_info);

clone_hook->op_array = parent_hook->op_array;
if (arg_info_substituted) {
clone_hook->op_array.arg_info = arg_info;
}
}
} ZEND_HASH_FOREACH_END();
Expand Down
18 changes: 18 additions & 0 deletions Zend/tests/generics/declaration/default_satisfies_bound.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
Declaration: type-parameter default that satisfies its bound is accepted
--FILE--
<?php
class A {}
class B extends A {}

class Box<T : A = B> {}
function f<T : A = B>(): void {}
trait Tr<T : A = B> {}
interface I<T : A = B> {}

new Box;
f();
echo "OK\n";
?>
--EXPECT--
OK
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
--TEST--
Declaration: class type-parameter default that does not satisfy its bound is rejected
--FILE--
<?php
class Animal {}
class Box<T : Animal = int> {}
?>
--EXPECTF--
Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
--TEST--
Declaration: function type-parameter default that does not satisfy its bound is rejected
--FILE--
<?php
class Animal {}
function id<T : Animal = int>(): void {}
?>
--EXPECTF--
Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
--TEST--
Declaration: interface type-parameter default that does not satisfy its bound is rejected
--FILE--
<?php
class Animal {}
interface I<T : Animal = int> {}
?>
--EXPECTF--
Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
--TEST--
Declaration: trait type-parameter default that does not satisfy its bound is rejected
--FILE--
<?php
class Animal {}
trait Holder<T : Animal = int> {}
?>
--EXPECTF--
Fatal error: Default int for type parameter T does not satisfy its bound Animal in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--TEST--
Declaration: default checked against intersection bound when types are concrete
--FILE--
<?php
class Box<T : Traversable & Countable = int> {}
?>
--EXPECTF--
Fatal error: Default int for type parameter T does not satisfy its bound Traversable&Countable in %s on line %d
8 changes: 8 additions & 0 deletions Zend/tests/generics/declaration/default_with_union_bound.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--TEST--
Declaration: default checked against union bound when types are concrete
--FILE--
<?php
class Box<T : int | string = float> {}
?>
--EXPECTF--
Fatal error: Default float for type parameter T does not satisfy its bound string|int in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
--TEST--
Bound error message: declaration-time default-vs-bound error renders NAMED_WITH_ARGS in the bound
--FILE--
<?php
interface Comparable<T> {}
class Box<T : Comparable<T> = int> {}
?>
--EXPECTF--
Fatal error: Default int for type parameter T does not satisfy its bound Comparable<T> in %s on line %d
17 changes: 17 additions & 0 deletions Zend/tests/generics/declaration/no_leak_dnf_bound.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
--TEST--
Generics: declaring a class with a DNF-typed bound does not leak memory
--FILE--
<?php
interface A {}
interface B {}
interface C {}

class Holder<T: (A&B)|C> {
public function take(T $x): void {}
public function get(): T {}
}

echo "ok\n";
?>
--EXPECT--
ok
15 changes: 15 additions & 0 deletions Zend/tests/generics/declaration/no_leak_intersection_bound.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--TEST--
Generics: declaring a class with an intersection-typed bound does not leak memory
--FILE--
<?php
interface A {}
interface B {}

class Holder<T: A&B> {
public function take(T $x): void {}
}

echo "ok\n";
?>
--EXPECT--
ok
15 changes: 15 additions & 0 deletions Zend/tests/generics/declaration/no_leak_union_bound.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--TEST--
Generics: declaring a class with a union-typed bound does not leak memory
--FILE--
<?php
interface A {}
interface B {}

class Holder<T: A|B> {
public function take(T $x): void {}
}

echo "ok\n";
?>
--EXPECT--
ok
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--TEST--
Recursive bounds: chain of forward references through three parameters
--FILE--
<?php
class Box<X> {}
class Foo {}
function f<T: Box<U>, U: Box<V>, V: Foo>(T $a, U $b, V $c): void {}
echo "ok\n";
?>
--EXPECT--
ok
Loading
Loading