Skip to content

Commit ae0ca5d

Browse files
committed
WIP
1 parent e398bbb commit ae0ca5d

47 files changed

Lines changed: 3156 additions & 219 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Zend/Optimizer/compact_literals.c

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,28 @@ static zend_string *create_str_cache_key(zval *literal, uint8_t num_related)
9494
Z_STRVAL_P(literal), Z_STRLEN_P(literal),
9595
Z_STRVAL_P(literal + 1), Z_STRLEN_P(literal + 1));
9696
} else if (num_related == 3) {
97-
ZEND_ASSERT(Z_TYPE_P(literal + 1) == IS_STRING && Z_TYPE_P(literal + 2) == IS_STRING);
98-
key = zend_string_concat3(
99-
Z_STRVAL_P(literal), Z_STRLEN_P(literal),
100-
Z_STRVAL_P(literal + 1), Z_STRLEN_P(literal + 1),
101-
Z_STRVAL_P(literal + 2), Z_STRLEN_P(literal + 2));
97+
if (Z_TYPE_P(literal + 2) == IS_PTR || Z_TYPE_P(literal + 2) == IS_LONG) {
98+
/* Generic args literal (IS_PTR or IS_LONG pointer) — include pointer value
99+
* in key to prevent merging across different generic instantiations */
100+
char ptr_buf[32];
101+
uintptr_t ptr_val = (Z_TYPE_P(literal + 2) == IS_PTR)
102+
? (uintptr_t)Z_PTR_P(literal + 2)
103+
: (uintptr_t)Z_LVAL_P(literal + 2);
104+
int ptr_len = snprintf(ptr_buf, sizeof(ptr_buf), "G%lx", (unsigned long)ptr_val);
105+
ZEND_ASSERT(Z_TYPE_P(literal + 1) == IS_STRING);
106+
size_t len = Z_STRLEN_P(literal) + Z_STRLEN_P(literal + 1) + ptr_len;
107+
key = zend_string_alloc(len, 0);
108+
memcpy(ZSTR_VAL(key), Z_STRVAL_P(literal), Z_STRLEN_P(literal));
109+
memcpy(ZSTR_VAL(key) + Z_STRLEN_P(literal), Z_STRVAL_P(literal + 1), Z_STRLEN_P(literal + 1));
110+
memcpy(ZSTR_VAL(key) + Z_STRLEN_P(literal) + Z_STRLEN_P(literal + 1), ptr_buf, ptr_len);
111+
ZSTR_VAL(key)[len] = '\0';
112+
} else {
113+
ZEND_ASSERT(Z_TYPE_P(literal + 1) == IS_STRING && Z_TYPE_P(literal + 2) == IS_STRING);
114+
key = zend_string_concat3(
115+
Z_STRVAL_P(literal), Z_STRLEN_P(literal),
116+
Z_STRVAL_P(literal + 1), Z_STRLEN_P(literal + 1),
117+
Z_STRVAL_P(literal + 2), Z_STRLEN_P(literal + 2));
118+
}
102119
} else {
103120
ZEND_ASSERT(0 && "Currently not needed");
104121
}
@@ -151,7 +168,8 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
151168
break;
152169
case ZEND_INIT_STATIC_METHOD_CALL:
153170
if (opline->op1_type == IS_CONST) {
154-
LITERAL_INFO(opline->op1.constant, 2);
171+
LITERAL_INFO(opline->op1.constant,
172+
(opline->result.num & 0x80000000) ? 3 : 2);
155173
}
156174
if (opline->op2_type == IS_CONST) {
157175
LITERAL_INFO(opline->op2.constant, 2);
@@ -201,14 +219,20 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
201219
}
202220
break;
203221
case ZEND_FETCH_CLASS:
204-
case ZEND_INSTANCEOF:
205222
if (opline->op2_type == IS_CONST) {
206223
LITERAL_INFO(opline->op2.constant, 2);
207224
}
208225
break;
226+
case ZEND_INSTANCEOF:
227+
if (opline->op2_type == IS_CONST) {
228+
LITERAL_INFO(opline->op2.constant,
229+
(opline->extended_value & ZEND_INSTANCEOF_GENERIC_FLAG) ? 3 : 2);
230+
}
231+
break;
209232
case ZEND_NEW:
210233
if (opline->op1_type == IS_CONST) {
211-
LITERAL_INFO(opline->op1.constant, 2);
234+
LITERAL_INFO(opline->op1.constant,
235+
(opline->op2.num & 0x80000000) ? 3 : 2);
212236
}
213237
break;
214238
case ZEND_DECLARE_CLASS:
@@ -569,30 +593,32 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
569593
}
570594
}
571595
break;
572-
case ZEND_INIT_STATIC_METHOD_CALL:
596+
case ZEND_INIT_STATIC_METHOD_CALL: {
597+
uint32_t generic_flag = opline->result.num & 0x80000000;
573598
if (opline->op2_type == IS_CONST) {
574599
// op2 static method
575600
if (opline->op1_type == IS_CONST) {
576601
opline->result.num = add_static_slot(&hash, op_array,
577602
opline->op1.constant,
578603
opline->op2.constant,
579604
LITERAL_STATIC_METHOD,
580-
&cache_size);
605+
&cache_size) | generic_flag;
581606
} else {
582-
opline->result.num = cache_size;
607+
opline->result.num = cache_size | generic_flag;
583608
cache_size += 2 * sizeof(void *);
584609
}
585610
} else if (opline->op1_type == IS_CONST) {
586611
// op1 class
587612
if (class_slot[opline->op1.constant] >= 0) {
588-
opline->result.num = class_slot[opline->op1.constant];
613+
opline->result.num = class_slot[opline->op1.constant] | generic_flag;
589614
} else {
590-
opline->result.num = cache_size;
615+
opline->result.num = cache_size | generic_flag;
591616
cache_size += sizeof(void *);
592-
class_slot[opline->op1.constant] = opline->result.num;
617+
class_slot[opline->op1.constant] = cache_size - sizeof(void *);
593618
}
594619
}
595620
break;
621+
}
596622
case ZEND_DEFINED:
597623
// op1 const
598624
if (const_slot[opline->op1.constant] >= 0) {
@@ -666,7 +692,6 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
666692
}
667693
break;
668694
case ZEND_FETCH_CLASS:
669-
case ZEND_INSTANCEOF:
670695
if (opline->op2_type == IS_CONST) {
671696
// op2 class
672697
if (class_slot[opline->op2.constant] >= 0) {
@@ -678,15 +703,29 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
678703
}
679704
}
680705
break;
706+
case ZEND_INSTANCEOF:
707+
if (opline->op2_type == IS_CONST) {
708+
// op2 class — preserve generic flag
709+
uint32_t generic_flag = opline->extended_value & ZEND_INSTANCEOF_GENERIC_FLAG;
710+
if (class_slot[opline->op2.constant] >= 0) {
711+
opline->extended_value = class_slot[opline->op2.constant] | generic_flag;
712+
} else {
713+
opline->extended_value = cache_size | generic_flag;
714+
cache_size += sizeof(void *);
715+
class_slot[opline->op2.constant] = cache_size - sizeof(void *);
716+
}
717+
}
718+
break;
681719
case ZEND_NEW:
682720
if (opline->op1_type == IS_CONST) {
683-
// op1 class
721+
// op1 class — preserve generic flag
722+
uint32_t generic_flag = opline->op2.num & 0x80000000;
684723
if (class_slot[opline->op1.constant] >= 0) {
685-
opline->op2.num = class_slot[opline->op1.constant];
724+
opline->op2.num = class_slot[opline->op1.constant] | generic_flag;
686725
} else {
687-
opline->op2.num = cache_size;
726+
opline->op2.num = cache_size | generic_flag;
688727
cache_size += sizeof(void *);
689-
class_slot[opline->op1.constant] = opline->op2.num;
728+
class_slot[opline->op1.constant] = cache_size - sizeof(void *);
690729
}
691730
}
692731
break;

Zend/Optimizer/zend_inference.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2383,6 +2383,11 @@ static uint32_t zend_convert_type(const zend_script *script, zend_type type, zen
23832383
return MAY_BE_ANY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF|MAY_BE_RC1|MAY_BE_RCN;
23842384
}
23852385

2386+
/* Generic type parameters can resolve to any type at runtime */
2387+
if (ZEND_TYPE_IS_GENERIC_PARAM(type)) {
2388+
return MAY_BE_ANY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF|MAY_BE_RC1|MAY_BE_RCN;
2389+
}
2390+
23862391
uint32_t tmp = zend_convert_type_declaration_mask(ZEND_TYPE_PURE_MASK(type));
23872392
if (ZEND_TYPE_IS_COMPLEX(type)) {
23882393
tmp |= MAY_BE_OBJECT;

Zend/Optimizer/zend_optimizer.c

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -301,22 +301,32 @@ bool zend_optimizer_update_op1_const(zend_op_array *op_array,
301301
opline->extended_value = alloc_cache_slots(op_array, 1);
302302
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
303303
break;
304-
case ZEND_NEW:
304+
case ZEND_NEW: {
305+
uint32_t generic_flag = opline->op2.num & 0x80000000;
305306
REQUIRES_STRING(val);
306307
drop_leading_backslash(val);
307308
opline->op1.constant = zend_optimizer_add_literal(op_array, val);
308-
opline->op2.num = alloc_cache_slots(op_array, 1);
309+
opline->op2.num = alloc_cache_slots(op_array, 1) | generic_flag;
309310
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
311+
if (generic_flag) {
312+
/* Copy the generic args IS_PTR literal (was at old op1.constant + 2) */
313+
/* Note: the generic args are already in the literal table from compilation,
314+
* but we need to add a placeholder since the optimizer rebuilds literals.
315+
* The actual IS_PTR will be re-added by the caller. */
316+
}
310317
break;
311-
case ZEND_INIT_STATIC_METHOD_CALL:
318+
}
319+
case ZEND_INIT_STATIC_METHOD_CALL: {
320+
uint32_t generic_flag = opline->result.num & 0x80000000;
312321
REQUIRES_STRING(val);
313322
drop_leading_backslash(val);
314323
opline->op1.constant = zend_optimizer_add_literal(op_array, val);
315324
if (opline->op2_type != IS_CONST) {
316-
opline->result.num = alloc_cache_slots(op_array, 1);
325+
opline->result.num = alloc_cache_slots(op_array, 1) | generic_flag;
317326
}
318327
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
319328
break;
329+
}
320330
case ZEND_FETCH_CLASS_CONSTANT:
321331
REQUIRES_STRING(val);
322332
drop_leading_backslash(val);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
--TEST--
2+
Generic class: anonymous classes extending generic classes
3+
--FILE--
4+
<?php
5+
6+
class Box<T> {
7+
public function __construct(public T $value) {}
8+
public function get(): T { return $this->value; }
9+
}
10+
11+
// Anonymous class extending generic with bound args
12+
$obj = new class(42) extends Box<int> {};
13+
echo $obj->get() . "\n";
14+
15+
// Type enforcement works
16+
try {
17+
$obj->value = "not an int";
18+
} catch (TypeError $e) {
19+
echo "TypeError: type enforced\n";
20+
}
21+
22+
// Anonymous class can override methods
23+
$obj2 = new class("hello") extends Box<string> {
24+
public function get(): string {
25+
return strtoupper(parent::get());
26+
}
27+
};
28+
echo $obj2->get() . "\n";
29+
30+
// Anonymous class implementing generic interface
31+
interface Getter<T> {
32+
public function get(): T;
33+
}
34+
35+
$obj3 = new class implements Getter<int> {
36+
public function get(): int { return 99; }
37+
};
38+
echo $obj3->get() . "\n";
39+
40+
echo "OK\n";
41+
?>
42+
--EXPECTF--
43+
42
44+
TypeError: type enforced
45+
HELLO
46+
99
47+
OK
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
--TEST--
2+
Generic class: autoloading triggers for base class name
3+
--FILE--
4+
<?php
5+
6+
$autoloaded = [];
7+
spl_autoload_register(function ($class) use (&$autoloaded) {
8+
$autoloaded[] = $class;
9+
if ($class === 'AutoBox') {
10+
eval('class AutoBox<T> { public function __construct(public T $value) {} public function get(): T { return $this->value; } }');
11+
}
12+
});
13+
14+
// new with generic args triggers autoload for base class
15+
$b = new AutoBox<int>(42);
16+
echo $b->get() . "\n";
17+
var_dump($b);
18+
19+
// Verify autoloader received the base class name (no generics)
20+
echo "Autoloaded: " . implode(", ", $autoloaded) . "\n";
21+
22+
echo "OK\n";
23+
?>
24+
--EXPECTF--
25+
42
26+
object(AutoBox<int>)#%d (1) {
27+
["value"]=>
28+
int(42)
29+
}
30+
Autoloaded: AutoBox
31+
OK
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
Generic class: class_alias works with generic classes
3+
--FILE--
4+
<?php
5+
6+
class Box<T> {
7+
public function __construct(public T $value) {}
8+
public function get(): T { return $this->value; }
9+
}
10+
11+
class_alias('Box', 'Container');
12+
13+
// Alias with generic args
14+
$c = new Container<int>(42);
15+
echo $c->get() . "\n";
16+
var_dump($c);
17+
18+
// get_class returns the original class name
19+
echo get_class($c) . "\n";
20+
21+
// instanceof works
22+
echo ($c instanceof Box) ? "instanceof Box: yes\n" : "instanceof Box: no\n";
23+
echo ($c instanceof Container) ? "instanceof Container: yes\n" : "instanceof Container: no\n";
24+
25+
// Type enforcement works through alias
26+
try {
27+
$c->value = "not an int";
28+
} catch (TypeError $e) {
29+
echo "TypeError: type enforced\n";
30+
}
31+
32+
echo "OK\n";
33+
?>
34+
--EXPECTF--
35+
42
36+
object(Box<int>)#1 (1) {
37+
["value"]=>
38+
int(42)
39+
}
40+
Box
41+
instanceof Box: yes
42+
instanceof Container: yes
43+
TypeError: type enforced
44+
OK
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
--TEST--
2+
Generic class: debug display with var_dump, print_r, and stack traces
3+
--FILE--
4+
<?php
5+
6+
class Box<T> {
7+
public function __construct(public T $value) {}
8+
public function throwError(): void {
9+
throw new RuntimeException("test");
10+
}
11+
}
12+
13+
class Pair<A, B> {
14+
public function __construct(public A $first, public B $second) {}
15+
}
16+
17+
// var_dump shows generic args
18+
$b = new Box<int>(42);
19+
var_dump($b);
20+
21+
// print_r shows generic args
22+
echo "---\n";
23+
$p = new Pair<string, int>("hello", 99);
24+
print_r($p);
25+
26+
// get_class returns raw class name (no generic args)
27+
echo "---\n";
28+
echo get_class($b) . "\n";
29+
echo get_class($p) . "\n";
30+
31+
// Stack trace shows generic args
32+
echo "---\n";
33+
try {
34+
$b->throwError();
35+
} catch (Throwable $e) {
36+
echo $e->getTraceAsString() . "\n";
37+
}
38+
39+
// Inherited class with bound generic args
40+
class IntBox extends Box<int> {}
41+
$ib = new IntBox(100);
42+
var_dump($ib);
43+
44+
echo "OK\n";
45+
?>
46+
--EXPECTF--
47+
object(Box<int>)#1 (1) {
48+
["value"]=>
49+
int(42)
50+
}
51+
---
52+
Pair<string, int> Object
53+
(
54+
[first] => hello
55+
[second] => 99
56+
)
57+
---
58+
Box
59+
Pair
60+
---
61+
#0 %s(%d): Box<int>->throwError()
62+
#1 {main}
63+
object(IntBox<int>)#%d (1) {
64+
["value"]=>
65+
int(100)
66+
}
67+
OK

0 commit comments

Comments
 (0)