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
12 changes: 12 additions & 0 deletions Zend/Optimizer/optimize_func_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "zend_constants.h"
#include "zend_execute.h"
#include "zend_vm.h"
#include "zend_generics.h"

typedef struct _optimizer_call_info {
zend_function *func;
Expand Down Expand Up @@ -131,6 +132,17 @@ static void zend_try_inline_call(zend_op_array *op_array, const zend_op *fcall,
MAKE_NOP(opline);
}

/* Release generic args literal before the INIT opcode is NOPed */
if (fcall->opcode == ZEND_INIT_STATIC_METHOD_CALL
&& fcall->op1_type == IS_CONST
&& (fcall->result.num & 0x80000000)) {
zval *generic_args_zv = &op_array->literals[fcall->op1.constant + 2];
if (Z_TYPE_P(generic_args_zv) == IS_PTR && Z_PTR_P(generic_args_zv) != NULL) {
zend_generic_args_release((zend_generic_args *) Z_PTR_P(generic_args_zv));
ZVAL_NULL(generic_args_zv);
}
}

zend_delete_call_instructions(op_array, opline-1);
}
}
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
61 changes: 61 additions & 0 deletions Zend/tests/generics/generic_abstract_class.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
--TEST--
Generic class: abstract generic class with abstract methods
--FILE--
<?php
declare(strict_types=1);

abstract class Repository<T> {
abstract public function find(int $id): T;
abstract public function save(T $entity): void;

public function findAndSave(int $id): T {
$item = $this->find($id);
$this->save($item);
return $item;
}
}

class User {
public function __construct(public int $id, public string $name) {}
}

class UserRepository extends Repository<User> {
private array $store = [];

public function find(int $id): User {
return $this->store[$id] ?? new User($id, "User$id");
}

public function save(User $entity): void {
$this->store[$entity->id] = $entity;
}
}

// 1. Basic usage
$repo = new UserRepository();
$user = $repo->find(1);
echo "1. " . $user->name . "\n";

// 2. Save and retrieve
$repo->save(new User(2, "Alice"));
echo "2. " . $repo->find(2)->name . "\n";

// 3. Template method pattern
$u = $repo->findAndSave(3);
echo "3. " . $u->name . "\n";

// 4. Cannot instantiate abstract generic class
try {
$r = new Repository<User>();
} catch (Error $e) {
echo "4. Error: " . (str_contains($e->getMessage(), 'abstract') ? 'abstract' : $e->getMessage()) . "\n";
}

echo "Done.\n";
?>
--EXPECT--
1. User1
2. Alice
3. User3
4. Error: abstract
Done.
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
Loading
Loading