Skip to content

Commit d311edd

Browse files
committed
Optimize generic args with refcounted sharing + fix type arg forwarding
Refcount zend_generic_args to eliminate per-object alloc/dealloc — new Box<int>() now adds a refcount instead of deep-copying, removing 4 allocator round-trips per object lifecycle. Inline resolved_masks into the args struct (single contiguous allocation). Fix crash when creating generic objects inside generic methods (new Box<T>() inside Factory<int> ::create()) by resolving type param refs from the enclosing context.
1 parent 1135bef commit d311edd

14 files changed

+314
-126
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
--TEST--
2+
Generic type argument forwarding (new Box<T> inside generic methods)
3+
--FILE--
4+
<?php
5+
declare(strict_types=1);
6+
7+
class Box<T> {
8+
public T $value;
9+
public function __construct(T $value) { $this->value = $value; }
10+
public function get(): T { return $this->value; }
11+
}
12+
13+
// Static method forwarding type args
14+
class Factory<T> {
15+
public static function create(T $val): Box<T> {
16+
return new Box<T>($val);
17+
}
18+
}
19+
20+
$box = Factory<int>::create(42);
21+
echo $box->get() . "\n";
22+
23+
$sbox = Factory<string>::create("hello");
24+
echo $sbox->get() . "\n";
25+
26+
// Instance method forwarding type args
27+
class Wrapper<T> {
28+
public function wrap(T $val): Box<T> {
29+
return new Box<T>($val);
30+
}
31+
}
32+
33+
$w = new Wrapper<int>();
34+
$b = $w->wrap(99);
35+
echo $b->get() . "\n";
36+
37+
// Type enforcement on forwarded object
38+
try {
39+
$box->value = "wrong";
40+
} catch (TypeError $e) {
41+
echo "TypeError caught\n";
42+
}
43+
44+
// Nested forwarding: Factory creates Wrapper which creates Box
45+
class NestedFactory<T> {
46+
public static function makeWrapped(T $val): Box<T> {
47+
$w = new Wrapper<T>();
48+
return $w->wrap($val);
49+
}
50+
}
51+
52+
$nb = NestedFactory<int>::makeWrapped(77);
53+
echo $nb->get() . "\n";
54+
55+
echo "OK\n";
56+
?>
57+
--EXPECT--
58+
42
59+
hello
60+
99
61+
TypeError caught
62+
77
63+
OK

Zend/zend_compile.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5437,7 +5437,7 @@ static void zend_compile_static_call(znode *result, zend_ast *ast, uint32_t type
54375437
has_generic_args_literal = true;
54385438
} else {
54395439
/* Non-const class references can't carry generic args in Phase 1 */
5440-
zend_generic_args_dtor(generic_args);
5440+
zend_generic_args_release(generic_args);
54415441
}
54425442
}
54435443

@@ -5541,7 +5541,7 @@ static void zend_compile_new(znode *result, zend_ast *ast) /* {{{ */
55415541
if (generic_args) {
55425542
/* For non-const class references, we can't easily pass generic args.
55435543
* For Phase 1, free and ignore (only const class names get generic args). */
5544-
zend_generic_args_dtor(generic_args);
5544+
zend_generic_args_release(generic_args);
55455545
}
55465546
}
55475547

@@ -11257,7 +11257,7 @@ static void zend_compile_instanceof(znode *result, zend_ast *ast) /* {{{ */
1125711257
if (obj_node.op_type == IS_CONST) {
1125811258
zend_do_free(&obj_node);
1125911259
if (generic_args) {
11260-
zend_generic_args_dtor(generic_args);
11260+
zend_generic_args_release(generic_args);
1126111261
}
1126211262
result->op_type = IS_CONST;
1126311263
ZVAL_FALSE(&result->u.constant);
@@ -11286,7 +11286,7 @@ static void zend_compile_instanceof(znode *result, zend_ast *ast) /* {{{ */
1128611286
SET_NODE(opline->op2, &class_node);
1128711287
if (generic_args) {
1128811288
/* Dynamic class ref with generics — not yet supported, free args */
11289-
zend_generic_args_dtor(generic_args);
11289+
zend_generic_args_release(generic_args);
1129011290
}
1129111291
}
1129211292
}

Zend/zend_execute.c

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,6 +1038,27 @@ static bool zend_check_and_resolve_property_or_class_constant_class_type(
10381038
return false;
10391039
}
10401040

1041+
static zend_always_inline zend_generic_args *i_zend_get_current_generic_args(void)
1042+
{
1043+
zend_execute_data *ex = EG(current_execute_data);
1044+
if (!ex) {
1045+
return NULL;
1046+
}
1047+
if (Z_TYPE(ex->This) == IS_OBJECT) {
1048+
zend_object *obj = Z_OBJ(ex->This);
1049+
if (obj->generic_args) {
1050+
return obj->generic_args;
1051+
}
1052+
if (obj->ce->bound_generic_args) {
1053+
return obj->ce->bound_generic_args;
1054+
}
1055+
}
1056+
if (EG(static_generic_args)) {
1057+
return EG(static_generic_args);
1058+
}
1059+
return NULL;
1060+
}
1061+
10411062
static zend_always_inline zend_generic_args *i_zend_get_generic_args_for_property(const zend_object *obj)
10421063
{
10431064
/* First try the object's own generic args */
@@ -1051,7 +1072,7 @@ static zend_always_inline zend_generic_args *i_zend_get_generic_args_for_propert
10511072
}
10521073
}
10531074
/* Fall back to execution context (for method calls on $this) */
1054-
return zend_get_current_generic_args();
1075+
return i_zend_get_current_generic_args();
10551076
}
10561077

10571078
static zend_always_inline bool i_zend_check_property_type(const zend_property_info *info, zval *property, bool strict, const zend_object *obj)
@@ -1064,8 +1085,8 @@ static zend_always_inline bool i_zend_check_property_type(const zend_property_in
10641085
zend_generic_args *args = i_zend_get_generic_args_for_property(obj);
10651086
if (args && gref->param_index < args->num_args) {
10661087
/* Fast path: use pre-computed mask for scalar types */
1067-
if (args->resolved_masks) {
1068-
uint32_t mask = args->resolved_masks[gref->param_index];
1088+
{
1089+
uint32_t mask = ZEND_GENERIC_ARGS_MASKS(args)[gref->param_index];
10691090
if (mask != 0 && ((1u << Z_TYPE_P(property)) & mask)) {
10701091
return 1;
10711092
}
@@ -1235,14 +1256,14 @@ static bool zend_check_intersection_type_from_list(
12351256
return true;
12361257
}
12371258

1238-
static zend_always_inline bool zend_check_type_slow(
1259+
static inline bool zend_check_type_slow(
12391260
const zend_type *type, zval *arg, const zend_reference *ref,
12401261
bool is_return_type, bool is_internal)
12411262
{
12421263
/* Handle generic type parameter references (e.g., T in a generic class) */
12431264
if (ZEND_TYPE_IS_GENERIC_PARAM(*type)) {
12441265
zend_generic_type_ref *gref = ZEND_TYPE_GENERIC_PARAM_REF(*type);
1245-
zend_generic_args *args = zend_get_current_generic_args();
1266+
zend_generic_args *args = i_zend_get_current_generic_args();
12461267

12471268
/* Lazy type inference: if no generic args are bound and we're in a
12481269
* constructor of a generic class, infer types from the actual arguments */
@@ -1260,8 +1281,8 @@ static zend_always_inline bool zend_check_type_slow(
12601281

12611282
if (args && gref->param_index < args->num_args) {
12621283
/* Fast path: use pre-computed mask for scalar types */
1263-
if (args->resolved_masks) {
1264-
uint32_t mask = args->resolved_masks[gref->param_index];
1284+
{
1285+
uint32_t mask = ZEND_GENERIC_ARGS_MASKS(args)[gref->param_index];
12651286
if (mask != 0 && ((1u << Z_TYPE_P(arg)) & mask)) {
12661287
return true;
12671288
}
@@ -1292,7 +1313,15 @@ static zend_always_inline bool zend_check_type_slow(
12921313
/* Check generic args match (respecting variance) */
12931314
zend_generic_args *obj_args = Z_OBJ_P(arg)->generic_args;
12941315
if (gcref->type_args && obj_args) {
1295-
return zend_generic_args_compatible(gcref->type_args, obj_args, expected_ce->generic_params_info, gcref->wildcard_bounds);
1316+
/* Resolve any generic param refs in expected type_args (e.g., Box<T> → Box<int>) */
1317+
zend_generic_args *resolved_expected = zend_resolve_generic_args_with_context(
1318+
gcref->type_args, i_zend_get_current_generic_args());
1319+
const zend_generic_args *expected_args = resolved_expected ? resolved_expected : gcref->type_args;
1320+
bool compatible = zend_generic_args_compatible(expected_args, obj_args, expected_ce->generic_params_info, gcref->wildcard_bounds);
1321+
if (resolved_expected) {
1322+
zend_generic_args_release(resolved_expected);
1323+
}
1324+
return compatible;
12961325
}
12971326
/* If no generic args on the object, just check the base class */
12981327
return true;

Zend/zend_generics.c

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -38,30 +38,26 @@ ZEND_API zend_generic_params_info *zend_alloc_generic_params_info(uint32_t num_p
3838

3939
ZEND_API zend_generic_args *zend_alloc_generic_args(uint32_t num_args)
4040
{
41-
zend_generic_args *args = safe_emalloc(
42-
num_args - 1, sizeof(zend_type),
43-
sizeof(zend_generic_args));
41+
zend_generic_args *args = emalloc(ZEND_GENERIC_ARGS_SIZE(num_args));
42+
args->refcount = 1;
4443
args->num_args = num_args;
45-
args->resolved_masks = NULL;
4644
for (uint32_t i = 0; i < num_args; i++) {
4745
args->args[i] = (zend_type) ZEND_TYPE_INIT_NONE(0);
4846
}
47+
memset(ZEND_GENERIC_ARGS_MASKS(args), 0, num_args * sizeof(uint32_t));
4948
return args;
5049
}
5150

5251
ZEND_API void zend_generic_args_compute_masks(zend_generic_args *args)
5352
{
54-
if (args->resolved_masks) {
55-
efree(args->resolved_masks);
56-
}
57-
args->resolved_masks = safe_emalloc(args->num_args, sizeof(uint32_t), 0);
53+
uint32_t *masks = ZEND_GENERIC_ARGS_MASKS(args);
5854
for (uint32_t i = 0; i < args->num_args; i++) {
5955
zend_type t = args->args[i];
6056
if (!ZEND_TYPE_HAS_NAME(t) && !ZEND_TYPE_IS_GENERIC_CLASS(t)
6157
&& !ZEND_TYPE_HAS_LIST(t) && !ZEND_TYPE_IS_GENERIC_PARAM(t)) {
62-
args->resolved_masks[i] = ZEND_TYPE_PURE_MASK(t) & MAY_BE_ANY;
58+
masks[i] = ZEND_TYPE_PURE_MASK(t) & MAY_BE_ANY;
6359
} else {
64-
args->resolved_masks[i] = 0; /* Complex type — requires slow path */
60+
masks[i] = 0; /* Complex type — requires slow path */
6561
}
6662
}
6763
}
@@ -100,20 +96,14 @@ ZEND_API zend_type zend_copy_generic_type(zend_type src)
10096
ZEND_API zend_generic_args *zend_copy_generic_args(const zend_generic_args *src)
10197
{
10298
uint32_t num_args = src->num_args;
103-
zend_generic_args *copy = safe_emalloc(
104-
num_args - 1, sizeof(zend_type),
105-
sizeof(zend_generic_args));
99+
zend_generic_args *copy = emalloc(ZEND_GENERIC_ARGS_SIZE(num_args));
100+
copy->refcount = 1;
106101
copy->num_args = num_args;
107102
for (uint32_t i = 0; i < num_args; i++) {
108103
copy->args[i] = zend_copy_generic_type(src->args[i]);
109104
}
110-
if (src->resolved_masks) {
111-
copy->resolved_masks = safe_emalloc(num_args, sizeof(uint32_t), 0);
112-
memcpy(copy->resolved_masks, src->resolved_masks, num_args * sizeof(uint32_t));
113-
} else {
114-
copy->resolved_masks = NULL;
115-
zend_generic_args_compute_masks(copy);
116-
}
105+
memcpy(ZEND_GENERIC_ARGS_MASKS(copy), ZEND_GENERIC_ARGS_MASKS(src),
106+
num_args * sizeof(uint32_t));
117107
return copy;
118108
}
119109

@@ -134,9 +124,6 @@ ZEND_API void zend_generic_args_dtor(zend_generic_args *args)
134124
for (uint32_t i = 0; i < args->num_args; i++) {
135125
zend_type_release(args->args[i], /* persistent */ 0);
136126
}
137-
if (args->resolved_masks) {
138-
efree(args->resolved_masks);
139-
}
140127
efree(args);
141128
}
142129

@@ -154,7 +141,7 @@ ZEND_API void zend_generic_class_ref_dtor(zend_generic_class_ref *ref)
154141
zend_string_release(ref->class_name);
155142
}
156143
if (ref->type_args) {
157-
zend_generic_args_dtor(ref->type_args);
144+
zend_generic_args_release(ref->type_args);
158145
}
159146
if (ref->wildcard_bounds) {
160147
efree(ref->wildcard_bounds);
@@ -173,6 +160,40 @@ ZEND_API zend_type zend_resolve_generic_type(zend_type type, const zend_generic_
173160
return type;
174161
}
175162

163+
ZEND_API zend_generic_args *zend_resolve_generic_args_with_context(
164+
const zend_generic_args *args, const zend_generic_args *context)
165+
{
166+
if (!context) {
167+
return NULL;
168+
}
169+
170+
/* Check if any args need resolution */
171+
bool needs_resolution = false;
172+
for (uint32_t i = 0; i < args->num_args; i++) {
173+
if (ZEND_TYPE_IS_GENERIC_PARAM(args->args[i])) {
174+
needs_resolution = true;
175+
break;
176+
}
177+
}
178+
if (!needs_resolution) {
179+
return NULL;
180+
}
181+
182+
/* Create resolved copy */
183+
zend_generic_args *resolved = zend_alloc_generic_args(args->num_args);
184+
for (uint32_t i = 0; i < args->num_args; i++) {
185+
if (ZEND_TYPE_IS_GENERIC_PARAM(args->args[i])) {
186+
zend_type resolved_type = zend_resolve_generic_type(args->args[i], context);
187+
/* Copy the resolved type (may still be a param ref if context doesn't have it) */
188+
resolved->args[i] = zend_copy_generic_type(resolved_type);
189+
} else {
190+
resolved->args[i] = zend_copy_generic_type(args->args[i]);
191+
}
192+
}
193+
zend_generic_args_compute_masks(resolved);
194+
return resolved;
195+
}
196+
176197
ZEND_API zend_generic_args *zend_expand_generic_args_with_defaults(
177198
const zend_generic_params_info *params, const zend_generic_args *args)
178199
{
@@ -461,13 +482,13 @@ ZEND_API bool zend_infer_generic_args_from_constructor(zend_object *obj, zend_ex
461482

462483
if (!all_inferred) {
463484
/* Could not infer all type params — free and leave unbound */
464-
zend_generic_args_dtor(inferred);
485+
zend_generic_args_release(inferred);
465486
return false;
466487
}
467488

468489
/* Validate against constraints */
469490
if (!zend_verify_generic_args(ce->generic_params_info, inferred)) {
470-
zend_generic_args_dtor(inferred);
491+
zend_generic_args_release(inferred);
471492
return false;
472493
}
473494

@@ -479,31 +500,21 @@ ZEND_API bool zend_infer_generic_args_from_constructor(zend_object *obj, zend_ex
479500
ZEND_API zend_generic_args *zend_get_current_generic_args(void)
480501
{
481502
zend_execute_data *ex = EG(current_execute_data);
482-
483503
if (!ex) {
484504
return NULL;
485505
}
486-
487-
/* For method calls: get from the object instance */
488506
if (Z_TYPE(ex->This) == IS_OBJECT) {
489507
zend_object *obj = Z_OBJ(ex->This);
490508
if (obj->generic_args) {
491509
return obj->generic_args;
492510
}
493-
/* Fall back to class-level bound args (e.g., IntList extends Collection<int>) */
494511
if (obj->ce->bound_generic_args) {
495512
return obj->ce->bound_generic_args;
496513
}
497514
}
498-
499-
/* For static method calls with generic args (e.g., Collection<int>::create()) */
500515
if (EG(static_generic_args)) {
501516
return EG(static_generic_args);
502517
}
503-
504-
/* For generic functions: stored in extra named args or similar mechanism */
505-
/* Phase 1: function-level generics use a simpler approach */
506-
507518
return NULL;
508519
}
509520

0 commit comments

Comments
 (0)