Skip to content
Draft
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 58 additions & 19 deletions Zend/Optimizer/compact_literals.c
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,28 @@ static zend_string *create_str_cache_key(zval *literal, uint8_t num_related)
Z_STRVAL_P(literal), Z_STRLEN_P(literal),
Z_STRVAL_P(literal + 1), Z_STRLEN_P(literal + 1));
} else if (num_related == 3) {
ZEND_ASSERT(Z_TYPE_P(literal + 1) == IS_STRING && Z_TYPE_P(literal + 2) == IS_STRING);
key = zend_string_concat3(
Z_STRVAL_P(literal), Z_STRLEN_P(literal),
Z_STRVAL_P(literal + 1), Z_STRLEN_P(literal + 1),
Z_STRVAL_P(literal + 2), Z_STRLEN_P(literal + 2));
if (Z_TYPE_P(literal + 2) == IS_PTR || Z_TYPE_P(literal + 2) == IS_LONG) {
/* Generic args literal (IS_PTR or IS_LONG pointer) — include pointer value
* in key to prevent merging across different generic instantiations */
char ptr_buf[32];
uintptr_t ptr_val = (Z_TYPE_P(literal + 2) == IS_PTR)
? (uintptr_t)Z_PTR_P(literal + 2)
: (uintptr_t)Z_LVAL_P(literal + 2);
int ptr_len = snprintf(ptr_buf, sizeof(ptr_buf), "G%lx", (unsigned long)ptr_val);
ZEND_ASSERT(Z_TYPE_P(literal + 1) == IS_STRING);
size_t len = Z_STRLEN_P(literal) + Z_STRLEN_P(literal + 1) + ptr_len;
key = zend_string_alloc(len, 0);
memcpy(ZSTR_VAL(key), Z_STRVAL_P(literal), Z_STRLEN_P(literal));
memcpy(ZSTR_VAL(key) + Z_STRLEN_P(literal), Z_STRVAL_P(literal + 1), Z_STRLEN_P(literal + 1));
memcpy(ZSTR_VAL(key) + Z_STRLEN_P(literal) + Z_STRLEN_P(literal + 1), ptr_buf, ptr_len);
ZSTR_VAL(key)[len] = '\0';
} else {
ZEND_ASSERT(Z_TYPE_P(literal + 1) == IS_STRING && Z_TYPE_P(literal + 2) == IS_STRING);
key = zend_string_concat3(
Z_STRVAL_P(literal), Z_STRLEN_P(literal),
Z_STRVAL_P(literal + 1), Z_STRLEN_P(literal + 1),
Z_STRVAL_P(literal + 2), Z_STRLEN_P(literal + 2));
}
} else {
ZEND_ASSERT(0 && "Currently not needed");
}
Expand Down Expand Up @@ -151,7 +168,8 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
break;
case ZEND_INIT_STATIC_METHOD_CALL:
if (opline->op1_type == IS_CONST) {
LITERAL_INFO(opline->op1.constant, 2);
LITERAL_INFO(opline->op1.constant,
(opline->result.num & 0x80000000) ? 3 : 2);
}
if (opline->op2_type == IS_CONST) {
LITERAL_INFO(opline->op2.constant, 2);
Expand Down Expand Up @@ -201,14 +219,20 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
}
break;
case ZEND_FETCH_CLASS:
case ZEND_INSTANCEOF:
if (opline->op2_type == IS_CONST) {
LITERAL_INFO(opline->op2.constant, 2);
}
break;
case ZEND_INSTANCEOF:
if (opline->op2_type == IS_CONST) {
LITERAL_INFO(opline->op2.constant,
(opline->extended_value & ZEND_INSTANCEOF_GENERIC_FLAG) ? 3 : 2);
}
break;
case ZEND_NEW:
if (opline->op1_type == IS_CONST) {
LITERAL_INFO(opline->op1.constant, 2);
LITERAL_INFO(opline->op1.constant,
(opline->op2.num & 0x80000000) ? 3 : 2);
}
break;
case ZEND_DECLARE_CLASS:
Expand Down Expand Up @@ -569,30 +593,32 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
}
}
break;
case ZEND_INIT_STATIC_METHOD_CALL:
case ZEND_INIT_STATIC_METHOD_CALL: {
uint32_t generic_flag = opline->result.num & 0x80000000;
if (opline->op2_type == IS_CONST) {
// op2 static method
if (opline->op1_type == IS_CONST) {
opline->result.num = add_static_slot(&hash, op_array,
opline->op1.constant,
opline->op2.constant,
LITERAL_STATIC_METHOD,
&cache_size);
&cache_size) | generic_flag;
} else {
opline->result.num = cache_size;
opline->result.num = cache_size | generic_flag;
cache_size += 2 * sizeof(void *);
}
} else if (opline->op1_type == IS_CONST) {
// op1 class
if (class_slot[opline->op1.constant] >= 0) {
opline->result.num = class_slot[opline->op1.constant];
opline->result.num = class_slot[opline->op1.constant] | generic_flag;
} else {
opline->result.num = cache_size;
opline->result.num = cache_size | generic_flag;
cache_size += sizeof(void *);
class_slot[opline->op1.constant] = opline->result.num;
class_slot[opline->op1.constant] = cache_size - sizeof(void *);
}
}
break;
}
case ZEND_DEFINED:
// op1 const
if (const_slot[opline->op1.constant] >= 0) {
Expand Down Expand Up @@ -666,7 +692,6 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
}
break;
case ZEND_FETCH_CLASS:
case ZEND_INSTANCEOF:
if (opline->op2_type == IS_CONST) {
// op2 class
if (class_slot[opline->op2.constant] >= 0) {
Expand All @@ -678,15 +703,29 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
}
}
break;
case ZEND_INSTANCEOF:
if (opline->op2_type == IS_CONST) {
// op2 class — preserve generic flag
uint32_t generic_flag = opline->extended_value & ZEND_INSTANCEOF_GENERIC_FLAG;
if (class_slot[opline->op2.constant] >= 0) {
opline->extended_value = class_slot[opline->op2.constant] | generic_flag;
} else {
opline->extended_value = cache_size | generic_flag;
cache_size += sizeof(void *);
class_slot[opline->op2.constant] = cache_size - sizeof(void *);
}
}
break;
case ZEND_NEW:
if (opline->op1_type == IS_CONST) {
// op1 class
// op1 class — preserve generic flag
uint32_t generic_flag = opline->op2.num & 0x80000000;
if (class_slot[opline->op1.constant] >= 0) {
opline->op2.num = class_slot[opline->op1.constant];
opline->op2.num = class_slot[opline->op1.constant] | generic_flag;
} else {
opline->op2.num = cache_size;
opline->op2.num = cache_size | generic_flag;
cache_size += sizeof(void *);
class_slot[opline->op1.constant] = opline->op2.num;
class_slot[opline->op1.constant] = cache_size - sizeof(void *);
}
}
break;
Expand Down
9 changes: 9 additions & 0 deletions Zend/Optimizer/zend_inference.c
Original file line number Diff line number Diff line change
Expand Up @@ -2383,6 +2383,11 @@ static uint32_t zend_convert_type(const zend_script *script, zend_type type, zen
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;
}

/* Generic type parameters can resolve to any type at runtime */
if (ZEND_TYPE_IS_GENERIC_PARAM(type)) {
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;
}

uint32_t tmp = zend_convert_type_declaration_mask(ZEND_TYPE_PURE_MASK(type));
if (ZEND_TYPE_IS_COMPLEX(type)) {
tmp |= MAY_BE_OBJECT;
Expand All @@ -2397,6 +2402,10 @@ static uint32_t zend_convert_type(const zend_script *script, zend_type type, zen
}
}
}
/* Generic class types (e.g., Box<int>) are always objects */
if (ZEND_TYPE_IS_GENERIC_CLASS(type)) {
tmp |= MAY_BE_OBJECT;
}
if (tmp & (MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE)) {
tmp |= MAY_BE_RC1 | MAY_BE_RCN;
}
Expand Down
12 changes: 9 additions & 3 deletions Zend/Optimizer/zend_optimizer.c
Original file line number Diff line number Diff line change
Expand Up @@ -301,22 +301,28 @@ bool zend_optimizer_update_op1_const(zend_op_array *op_array,
opline->extended_value = alloc_cache_slots(op_array, 1);
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
break;
case ZEND_NEW:
case ZEND_NEW: {
REQUIRES_STRING(val);
drop_leading_backslash(val);
/* Non-const ZEND_NEW converted to const can never have generic args.
* Generic args are only added during compilation for const class names.
* Clear the flag to prevent false positives from uninitialized op2.num. */
opline->op1.constant = zend_optimizer_add_literal(op_array, val);
opline->op2.num = alloc_cache_slots(op_array, 1);
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
break;
case ZEND_INIT_STATIC_METHOD_CALL:
}
case ZEND_INIT_STATIC_METHOD_CALL: {
uint32_t generic_flag = opline->result.num & 0x80000000;
REQUIRES_STRING(val);
drop_leading_backslash(val);
opline->op1.constant = zend_optimizer_add_literal(op_array, val);
if (opline->op2_type != IS_CONST) {
opline->result.num = alloc_cache_slots(op_array, 1);
opline->result.num = alloc_cache_slots(op_array, 1) | generic_flag;
}
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
break;
}
case ZEND_FETCH_CLASS_CONSTANT:
REQUIRES_STRING(val);
drop_leading_backslash(val);
Expand Down
47 changes: 47 additions & 0 deletions Zend/tests/generics/generic_anonymous_class.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
--TEST--
Generic class: anonymous classes extending generic classes
--FILE--
<?php

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

// Anonymous class extending generic with bound args
$obj = new class(42) extends Box<int> {};
echo $obj->get() . "\n";

// Type enforcement works
try {
$obj->value = "not an int";
} catch (TypeError $e) {
echo "TypeError: type enforced\n";
}

// Anonymous class can override methods
$obj2 = new class("hello") extends Box<string> {
public function get(): string {
return strtoupper(parent::get());
}
};
echo $obj2->get() . "\n";

// Anonymous class implementing generic interface
interface Getter<T> {
public function get(): T;
}

$obj3 = new class implements Getter<int> {
public function get(): int { return 99; }
};
echo $obj3->get() . "\n";

echo "OK\n";
?>
--EXPECTF--
42
TypeError: type enforced
HELLO
99
OK
31 changes: 31 additions & 0 deletions Zend/tests/generics/generic_autoloading.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
Generic class: autoloading triggers for base class name
--FILE--
<?php

$autoloaded = [];
spl_autoload_register(function ($class) use (&$autoloaded) {
$autoloaded[] = $class;
if ($class === 'AutoBox') {
eval('class AutoBox<T> { public function __construct(public T $value) {} public function get(): T { return $this->value; } }');
}
});

// new with generic args triggers autoload for base class
$b = new AutoBox<int>(42);
echo $b->get() . "\n";
var_dump($b);

// Verify autoloader received the base class name (no generics)
echo "Autoloaded: " . implode(", ", $autoloaded) . "\n";

echo "OK\n";
?>
--EXPECTF--
42
object(AutoBox<int>)#%d (1) {
["value"]=>
int(42)
}
Autoloaded: AutoBox
OK
44 changes: 44 additions & 0 deletions Zend/tests/generics/generic_class_alias.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
--TEST--
Generic class: class_alias works with generic classes
--FILE--
<?php

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

class_alias('Box', 'Container');

// Alias with generic args
$c = new Container<int>(42);
echo $c->get() . "\n";
var_dump($c);

// get_class returns the original class name
echo get_class($c) . "\n";

// instanceof works
echo ($c instanceof Box) ? "instanceof Box: yes\n" : "instanceof Box: no\n";
echo ($c instanceof Container) ? "instanceof Container: yes\n" : "instanceof Container: no\n";

// Type enforcement works through alias
try {
$c->value = "not an int";
} catch (TypeError $e) {
echo "TypeError: type enforced\n";
}

echo "OK\n";
?>
--EXPECTF--
42
object(Box<int>)#1 (1) {
["value"]=>
int(42)
}
Box
instanceof Box: yes
instanceof Container: yes
TypeError: type enforced
OK
19 changes: 19 additions & 0 deletions Zend/tests/generics/generic_class_basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
--TEST--
Generic class: basic declaration and instantiation
--FILE--
<?php
class Box<T> {
private $value;
public function __construct(T $value) { $this->value = $value; }
public function get(): T { return $this->value; }
}

$box = new Box(42);
echo $box->get() . "\n";

$box2 = new Box("hello");
echo $box2->get() . "\n";
?>
--EXPECT--
42
hello
22 changes: 22 additions & 0 deletions Zend/tests/generics/generic_class_constrained_param.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--TEST--
Generic class: constrained type parameter with upper bound
--FILE--
<?php
class TypedCollection<T: Countable> {
private array $items = [];
public function add(T $item): void {
$this->items[] = $item;
}
public function count(): int {
return count($this->items);
}
}

echo "TypedCollection declared\n";

$c = new TypedCollection();
echo "Instantiated\n";
?>
--EXPECT--
TypedCollection declared
Instantiated
24 changes: 24 additions & 0 deletions Zend/tests/generics/generic_class_inheritance.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--TEST--
Generic class: inheritance with bound type arguments
--FILE--
<?php
class Box<T> {
private $value;
public function __construct(T $value) { $this->value = $value; }
public function get(): T { return $this->value; }
}

class IntBox extends Box<int> {}

$ib = new IntBox(99);
echo $ib->get() . "\n";

try {
$bad = new IntBox("not an int");
} catch (TypeError $e) {
echo "TypeError caught\n";
}
?>
--EXPECT--
99
TypeError caught
Loading
Loading