diff --git a/Zend/Optimizer/compact_literals.c b/Zend/Optimizer/compact_literals.c index 447a034530e1b..39b30779fa11a 100644 --- a/Zend/Optimizer/compact_literals.c +++ b/Zend/Optimizer/compact_literals.c @@ -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"); } @@ -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); @@ -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: @@ -569,7 +593,8 @@ 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) { @@ -577,22 +602,23 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx 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) { @@ -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) { @@ -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; diff --git a/Zend/Optimizer/zend_inference.c b/Zend/Optimizer/zend_inference.c index 6963fc63dd15c..2259a3c17d5e3 100644 --- a/Zend/Optimizer/zend_inference.c +++ b/Zend/Optimizer/zend_inference.c @@ -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; @@ -2397,6 +2402,10 @@ static uint32_t zend_convert_type(const zend_script *script, zend_type type, zen } } } + /* Generic class types (e.g., Box) 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; } diff --git a/Zend/Optimizer/zend_optimizer.c b/Zend/Optimizer/zend_optimizer.c index f8cbefdaaf2b5..8f6de7ddb930c 100644 --- a/Zend/Optimizer/zend_optimizer.c +++ b/Zend/Optimizer/zend_optimizer.c @@ -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); diff --git a/Zend/tests/generics/generic_abstract_class.phpt b/Zend/tests/generics/generic_abstract_class.phpt new file mode 100644 index 0000000000000..07e2b44033a0d --- /dev/null +++ b/Zend/tests/generics/generic_abstract_class.phpt @@ -0,0 +1,61 @@ +--TEST-- +Generic class: abstract generic class with abstract methods +--FILE-- + { + 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 { + 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(); +} 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. diff --git a/Zend/tests/generics/generic_anonymous_class.phpt b/Zend/tests/generics/generic_anonymous_class.phpt new file mode 100644 index 0000000000000..21a6137e45384 --- /dev/null +++ b/Zend/tests/generics/generic_anonymous_class.phpt @@ -0,0 +1,47 @@ +--TEST-- +Generic class: anonymous classes extending generic classes +--FILE-- + { + 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 {}; +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 { + public function get(): string { + return strtoupper(parent::get()); + } +}; +echo $obj2->get() . "\n"; + +// Anonymous class implementing generic interface +interface Getter { + public function get(): T; +} + +$obj3 = new class implements Getter { + public function get(): int { return 99; } +}; +echo $obj3->get() . "\n"; + +echo "OK\n"; +?> +--EXPECTF-- +42 +TypeError: type enforced +HELLO +99 +OK diff --git a/Zend/tests/generics/generic_autoloading.phpt b/Zend/tests/generics/generic_autoloading.phpt new file mode 100644 index 0000000000000..7c1e006e16d53 --- /dev/null +++ b/Zend/tests/generics/generic_autoloading.phpt @@ -0,0 +1,31 @@ +--TEST-- +Generic class: autoloading triggers for base class name +--FILE-- + { 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(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)#%d (1) { + ["value"]=> + int(42) +} +Autoloaded: AutoBox +OK diff --git a/Zend/tests/generics/generic_circular_ref.phpt b/Zend/tests/generics/generic_circular_ref.phpt new file mode 100644 index 0000000000000..f97e1771b7faa --- /dev/null +++ b/Zend/tests/generics/generic_circular_ref.phpt @@ -0,0 +1,75 @@ +--TEST-- +Generic class: circular references with GC +--FILE-- + { + public T $value; + public ?self $next = null; + + public function __construct(T $value) { + $this->value = $value; + } +} + +// 1. Simple circular reference +$a = new Node(1); +$b = new Node(2); +$a->next = $b; +$b->next = $a; // circular + +echo "1. a=" . $a->value . ", b=" . $b->value . "\n"; +echo "1. a->next=" . $a->next->value . "\n"; +echo "1. b->next=" . $b->next->value . "\n"; + +// Break and let GC handle it +unset($a, $b); +gc_collect_cycles(); +echo "1. GC OK\n"; + +// 2. Self-referential +$self = new Node(42); +$self->next = $self; +echo "2. self=" . $self->value . "\n"; +echo "2. self->next=" . $self->next->value . "\n"; +unset($self); +gc_collect_cycles(); +echo "2. GC OK\n"; + +// 3. Longer cycle +$n1 = new Node("a"); +$n2 = new Node("b"); +$n3 = new Node("c"); +$n1->next = $n2; +$n2->next = $n3; +$n3->next = $n1; // cycle + +echo "3. chain: " . $n1->value . "->" . $n2->value . "->" . $n3->value . "\n"; +unset($n1, $n2, $n3); +gc_collect_cycles(); +echo "3. GC OK\n"; + +// 4. Many cycles to stress GC +for ($i = 0; $i < 100; $i++) { + $x = new Node($i); + $y = new Node($i + 1); + $x->next = $y; + $y->next = $x; +} +gc_collect_cycles(); +echo "4. 100 cycles GC OK\n"; + +echo "Done.\n"; +?> +--EXPECT-- +1. a=1, b=2 +1. a->next=2 +1. b->next=1 +1. GC OK +2. self=42 +2. self->next=42 +2. GC OK +3. chain: a->b->c +3. GC OK +4. 100 cycles GC OK +Done. diff --git a/Zend/tests/generics/generic_class_alias.phpt b/Zend/tests/generics/generic_class_alias.phpt new file mode 100644 index 0000000000000..a20ac44f9fa56 --- /dev/null +++ b/Zend/tests/generics/generic_class_alias.phpt @@ -0,0 +1,44 @@ +--TEST-- +Generic class: class_alias works with generic classes +--FILE-- + { + public function __construct(public T $value) {} + public function get(): T { return $this->value; } +} + +class_alias('Box', 'Container'); + +// Alias with generic args +$c = new Container(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)#1 (1) { + ["value"]=> + int(42) +} +Box +instanceof Box: yes +instanceof Container: yes +TypeError: type enforced +OK diff --git a/Zend/tests/generics/generic_class_basic.phpt b/Zend/tests/generics/generic_class_basic.phpt new file mode 100644 index 0000000000000..17e1de8ce1a15 --- /dev/null +++ b/Zend/tests/generics/generic_class_basic.phpt @@ -0,0 +1,19 @@ +--TEST-- +Generic class: basic declaration and instantiation +--FILE-- + { + 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 diff --git a/Zend/tests/generics/generic_class_constrained_param.phpt b/Zend/tests/generics/generic_class_constrained_param.phpt new file mode 100644 index 0000000000000..bd1e95e0d542a --- /dev/null +++ b/Zend/tests/generics/generic_class_constrained_param.phpt @@ -0,0 +1,22 @@ +--TEST-- +Generic class: constrained type parameter with upper bound +--FILE-- + { + 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 diff --git a/Zend/tests/generics/generic_class_inheritance.phpt b/Zend/tests/generics/generic_class_inheritance.phpt new file mode 100644 index 0000000000000..8e030159290f7 --- /dev/null +++ b/Zend/tests/generics/generic_class_inheritance.phpt @@ -0,0 +1,24 @@ +--TEST-- +Generic class: inheritance with bound type arguments +--FILE-- + { + private $value; + public function __construct(T $value) { $this->value = $value; } + public function get(): T { return $this->value; } +} + +class IntBox extends Box {} + +$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 diff --git a/Zend/tests/generics/generic_class_multiple_params.phpt b/Zend/tests/generics/generic_class_multiple_params.phpt new file mode 100644 index 0000000000000..6c35890286075 --- /dev/null +++ b/Zend/tests/generics/generic_class_multiple_params.phpt @@ -0,0 +1,22 @@ +--TEST-- +Generic class: multiple type parameters +--FILE-- + { + private $first; + private $second; + public function __construct(A $first, B $second) { + $this->first = $first; + $this->second = $second; + } + public function getFirst(): A { return $this->first; } + public function getSecond(): B { return $this->second; } +} + +$pair = new Pair("hello", 42); +echo $pair->getFirst() . "\n"; +echo $pair->getSecond() . "\n"; +?> +--EXPECT-- +hello +42 diff --git a/Zend/tests/generics/generic_class_new_with_type_args.phpt b/Zend/tests/generics/generic_class_new_with_type_args.phpt new file mode 100644 index 0000000000000..81790ffff5214 --- /dev/null +++ b/Zend/tests/generics/generic_class_new_with_type_args.phpt @@ -0,0 +1,19 @@ +--TEST-- +Generic class: new with explicit type arguments +--FILE-- + { + private $value; + public function __construct(T $value) { $this->value = $value; } + public function get(): T { return $this->value; } +} + +$intBox = new Box(42); +echo $intBox->get() . "\n"; + +$strBox = new Box("hello"); +echo $strBox->get() . "\n"; +?> +--EXPECT-- +42 +hello diff --git a/Zend/tests/generics/generic_class_type_enforcement.phpt b/Zend/tests/generics/generic_class_type_enforcement.phpt new file mode 100644 index 0000000000000..6a573eb2ad6a1 --- /dev/null +++ b/Zend/tests/generics/generic_class_type_enforcement.phpt @@ -0,0 +1,18 @@ +--TEST-- +Generic class: type enforcement on constructor parameter +--FILE-- + { + private $value; + public function __construct(T $value) { $this->value = $value; } + public function get(): T { return $this->value; } +} + +try { + $box = new Box("not an int"); +} catch (TypeError $e) { + echo "TypeError: " . $e->getMessage() . "\n"; +} +?> +--EXPECTF-- +TypeError: Box::__construct(): Argument #1 ($value) must be of type int, string given, called in %s on line %d diff --git a/Zend/tests/generics/generic_clone.phpt b/Zend/tests/generics/generic_clone.phpt new file mode 100644 index 0000000000000..2608906746d0d --- /dev/null +++ b/Zend/tests/generics/generic_clone.phpt @@ -0,0 +1,29 @@ +--TEST-- +Generic class: clone preserves generic type arguments +--FILE-- + { + public T $value; + public function __construct(T $value) { $this->value = $value; } +} + +$a = new Box(42); +$b = clone $a; + +echo $b->value . "\n"; + +// Clone should preserve generic args — assigning wrong type should fail +try { + $b->value = "hello"; +} catch (TypeError $e) { + echo $e->getMessage() . "\n"; +} + +echo "OK\n"; +?> +--EXPECTF-- +42 +Cannot assign string to property Box::$value of type int +OK diff --git a/Zend/tests/generics/generic_closure.phpt b/Zend/tests/generics/generic_closure.phpt new file mode 100644 index 0000000000000..3b20b2fab63ad --- /dev/null +++ b/Zend/tests/generics/generic_closure.phpt @@ -0,0 +1,72 @@ +--TEST-- +Generic closures and arrow functions +--FILE-- +(T $value): T { + return $value; +}; + +echo $identity(42) . "\n"; // 42 +echo $identity("hello") . "\n"; // hello + +// Generic arrow function +$wrap = fn(T $x): array => [$x]; +var_dump($wrap(42)); // array(1) { [0]=> int(42) } +var_dump($wrap("hello")); // array(1) { [0]=> string(5) "hello" } + +// Generic closure with constraint +$count = function(T $item): int { + return count($item); +}; + +echo $count(new ArrayObject([1, 2, 3])) . "\n"; // 3 + +// Multiple type params +$pair = fn(A $a, B $b): array => [$a, $b]; +var_dump($pair(1, "two")); // array(2) { [0]=> int(1) [1]=> string(3) "two" } + +// Reflection on generic closures +$rf = new ReflectionFunction($identity); +var_dump($rf->isGeneric()); // true +$params = $rf->getGenericParameters(); +echo "identity generic params: " . count($params) . "\n"; +echo " T: " . $params[0]->getName() . "\n"; + +// Reflection on generic arrow function +$rf2 = new ReflectionFunction($wrap); +var_dump($rf2->isGeneric()); // true + +// Non-generic closure for comparison +$plain = function(int $x): int { return $x * 2; }; +$rf3 = new ReflectionFunction($plain); +var_dump($rf3->isGeneric()); // false + +echo "OK\n"; +?> +--EXPECT-- +42 +hello +array(1) { + [0]=> + int(42) +} +array(1) { + [0]=> + string(5) "hello" +} +3 +array(2) { + [0]=> + int(1) + [1]=> + string(3) "two" +} +bool(true) +identity generic params: 1 + T: T +bool(true) +bool(false) +OK diff --git a/Zend/tests/generics/generic_closure_binding.phpt b/Zend/tests/generics/generic_closure_binding.phpt new file mode 100644 index 0000000000000..4b09b20860da1 --- /dev/null +++ b/Zend/tests/generics/generic_closure_binding.phpt @@ -0,0 +1,61 @@ +--TEST-- +Generic class: Closure::bind and bindTo with generic $this +--FILE-- + { + private T $value; + public function __construct(T $value) { $this->value = $value; } + public function get(): T { return $this->value; } +} + +// 1. Closure reading private property of generic object +$getter = Closure::bind(function() { + return $this->value; +}, new Box(42), Box::class); + +echo "1. " . $getter() . "\n"; + +// 2. Closure bound to different generic instance +$box1 = new Box("hello"); +$box2 = new Box("world"); + +$fn = function() { return $this->value; }; +$bound1 = Closure::bind($fn, $box1, Box::class); +$bound2 = Closure::bind($fn, $box2, Box::class); + +echo "2. " . $bound1() . "\n"; +echo "2. " . $bound2() . "\n"; + +// 3. bindTo +$box3 = new Box(99); +$rebound = $bound1->bindTo($box3, Box::class); +echo "3. " . $rebound() . "\n"; + +// 4. Closure with use() capturing generic object +$captured = new Box(7); +$fn = function() use ($captured) { + return $captured->get(); +}; +echo "4. " . $fn() . "\n"; + +// 5. Closure modifying generic object +$modifier = Closure::bind(function($val) { + $this->value = $val; +}, new Box(0), Box::class); + +$modifier(42); +// Note: the closure's $this is a copy of the original binding +echo "5. OK\n"; + +echo "Done.\n"; +?> +--EXPECT-- +1. 42 +2. hello +2. world +3. 99 +4. 7 +5. OK +Done. diff --git a/Zend/tests/generics/generic_constraint_enforcement.phpt b/Zend/tests/generics/generic_constraint_enforcement.phpt new file mode 100644 index 0000000000000..cea7261060ef8 --- /dev/null +++ b/Zend/tests/generics/generic_constraint_enforcement.phpt @@ -0,0 +1,23 @@ +--TEST-- +Generic class: constraint enforcement at instantiation +--FILE-- + { + private array $items = []; + public function add(T $item): void { + $this->items[] = $item; + } +} + +// ArrayObject implements Countable — should work +$c = new TypedCollection(); +echo "ArrayObject accepted\n"; + +// string does NOT implement Countable — should fatal error +$c2 = new TypedCollection(); +echo "Should not reach here\n"; +?> +--EXPECTF-- +ArrayObject accepted + +Fatal error: Generic type argument #1 must implement Countable, string given in %s on line %d diff --git a/Zend/tests/generics/generic_contravariance_error.phpt b/Zend/tests/generics/generic_contravariance_error.phpt new file mode 100644 index 0000000000000..32855b13be882 --- /dev/null +++ b/Zend/tests/generics/generic_contravariance_error.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic class: contravariance (in) compile error when used in return position +--FILE-- + { + public function get(): T { return null; } +} +?> +--EXPECTF-- +Fatal error: Contravariant type parameter T of class BadConsumer cannot be used in return type of method BadConsumer::get() (only parameter types allowed) in %s on line %d diff --git a/Zend/tests/generics/generic_contravariance_subtype.phpt b/Zend/tests/generics/generic_contravariance_subtype.phpt new file mode 100644 index 0000000000000..d7d04f0ad8274 --- /dev/null +++ b/Zend/tests/generics/generic_contravariance_subtype.phpt @@ -0,0 +1,34 @@ +--TEST-- +Generic class: contravariant subtyping (Consumer assignable to Consumer) +--FILE-- +name = $name; } +} +class Dog extends Animal {} + +class Consumer { + private mixed $callback; + public function __construct(mixed $callback) { $this->callback = $callback; } + public function consume(T $item): void { + echo ($this->callback)($item) . "\n"; + } +} + +function feedDog(Consumer $consumer): void { + $consumer->consume(new Dog("Rex")); +} + +// Contravariant: Consumer should be assignable to Consumer +// because a consumer of Animals can consume any Dog (Dog is an Animal) +$animalConsumer = new Consumer(function(Animal $a) { return "Consumed: " . $a->name; }); +feedDog($animalConsumer); + +echo "OK\n"; +?> +--EXPECT-- +Consumed: Rex +OK diff --git a/Zend/tests/generics/generic_covariance_basic.phpt b/Zend/tests/generics/generic_covariance_basic.phpt new file mode 100644 index 0000000000000..1b1ac655c3eb6 --- /dev/null +++ b/Zend/tests/generics/generic_covariance_basic.phpt @@ -0,0 +1,26 @@ +--TEST-- +Generic class: covariance (out) annotation and compile-time validation +--FILE-- + { + private mixed $value; + public function __construct(mixed $value) { $this->value = $value; } + public function get(): T { return $this->value; } +} + +$dogBox = new ReadOnlyBox(new Dog()); +$dog = $dogBox->get(); +echo get_class($dog) . "\n"; + +echo "OK\n"; +?> +--EXPECT-- +Dog +OK diff --git a/Zend/tests/generics/generic_covariance_error.phpt b/Zend/tests/generics/generic_covariance_error.phpt new file mode 100644 index 0000000000000..d7c0995284f14 --- /dev/null +++ b/Zend/tests/generics/generic_covariance_error.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic class: covariance (out) compile error when used in parameter position +--FILE-- + { + public function set(T $value): void {} +} +?> +--EXPECTF-- +Fatal error: Covariant type parameter T of class BadBox cannot be used in parameter $value of method BadBox::set() (only return types allowed) in %s on line %d diff --git a/Zend/tests/generics/generic_covariance_subtype.phpt b/Zend/tests/generics/generic_covariance_subtype.phpt new file mode 100644 index 0000000000000..c40e4cab4a06f --- /dev/null +++ b/Zend/tests/generics/generic_covariance_subtype.phpt @@ -0,0 +1,36 @@ +--TEST-- +Generic class: covariant subtyping (ReadOnlyBox assignable to ReadOnlyBox) +--FILE-- +name = $name; } +} +class Dog extends Animal {} +class Cat extends Animal {} + +class ReadOnlyBox { + private mixed $value; + public function __construct(mixed $value) { $this->value = $value; } + public function get(): T { return $this->value; } +} + +function processAnimalBox(ReadOnlyBox $box): string { + return $box->get()->name; +} + +// Covariant: ReadOnlyBox should be assignable to ReadOnlyBox +$dogBox = new ReadOnlyBox(new Dog("Rex")); +echo processAnimalBox($dogBox) . "\n"; + +$catBox = new ReadOnlyBox(new Cat("Whiskers")); +echo processAnimalBox($catBox) . "\n"; + +echo "OK\n"; +?> +--EXPECT-- +Rex +Whiskers +OK diff --git a/Zend/tests/generics/generic_cross_file_include.phpt b/Zend/tests/generics/generic_cross_file_include.phpt new file mode 100644 index 0000000000000..bee7e6cf68b63 --- /dev/null +++ b/Zend/tests/generics/generic_cross_file_include.phpt @@ -0,0 +1,71 @@ +--TEST-- +Generic class: generic class defined in included file +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +--FILE-- + { + public T $value; + public function __construct(T $value) { $this->value = $value; } + public function get(): T { return $this->value; } +} +'); + +file_put_contents($file2, ' { + public function __construct(private A $first, private B $second) {} + public function first(): A { return $this->first; } + public function second(): B { return $this->second; } +} +'); + +// Include both files +require_once $file1; +require_once $file2; + +// 1. Use included generic class +$box = new Box(42); +echo "1. " . $box->get() . "\n"; + +// 2. Type enforcement from included class +try { + $box2 = new Box("bad"); +} catch (TypeError $e) { + echo "2. TypeError OK\n"; +} + +// 3. Second included generic class +$pair = new Pair("age", 25); +echo "3. " . $pair->first() . ": " . $pair->second() . "\n"; + +// 4. Inheritance from included generic class +class IntBox extends Box {} +$ib = new IntBox(99); +echo "4. " . $ib->get() . "\n"; + +// 5. Type inference from included class +$inferred = new Box("hello"); +echo "5. " . $inferred->get() . "\n"; + +// Cleanup +unlink($file1); +unlink($file2); + +echo "Done.\n"; +?> +--EXPECT-- +1. 42 +2. TypeError OK +3. age: 25 +4. 99 +5. hello +Done. diff --git a/Zend/tests/generics/generic_ctor_inheritance.phpt b/Zend/tests/generics/generic_ctor_inheritance.phpt new file mode 100644 index 0000000000000..b40b59e392503 --- /dev/null +++ b/Zend/tests/generics/generic_ctor_inheritance.phpt @@ -0,0 +1,77 @@ +--TEST-- +Generic class: child inherits parent's generic constructor +--FILE-- + { + private T $value; + public function __construct(T $value) { $this->value = $value; } + public function get(): T { return $this->value; } +} + +// Child with no constructor — inherits parent's +class LabeledBox extends Box { + public function label(): string { return "LabeledBox"; } +} + +// Child with bound type and no constructor +class IntBox extends Box {} + +// Child with own constructor calling parent +class NamedBox extends Box { + private string $name; + public function __construct(string $name, T $value) { + $this->name = $name; + parent::__construct($value); + } + public function getName(): string { return $this->name; } +} + +// 1. Inherited constructor with explicit type args +$lb = new LabeledBox("hello"); +echo "1. " . $lb->get() . " - " . $lb->label() . "\n"; + +// 2. Inherited constructor — type enforcement +try { + $lb2 = new LabeledBox("not int"); +} catch (TypeError $e) { + echo "2. TypeError OK\n"; +} + +// 3. Bound type with inherited constructor +$ib = new IntBox(42); +echo "3. " . $ib->get() . "\n"; + +try { + $ib2 = new IntBox("not int"); +} catch (TypeError $e) { + echo "3. TypeError OK\n"; +} + +// 4. Own constructor calling parent +$nb = new NamedBox("temp", 98.6); +echo "4. " . $nb->getName() . ": " . $nb->get() . "\n"; + +// 5. Inferred type through explicit type args on child +$lb3 = new LabeledBox(42); +echo "5. " . $lb3->get() . " - " . $lb3->label() . "\n"; + +try { + $lb3->get(); // returns int, fine + echo "5. class: " . get_class($lb3) . "\n"; +} catch (TypeError $e) { + echo "FAIL: " . $e->getMessage() . "\n"; +} + +echo "Done.\n"; +?> +--EXPECT-- +1. hello - LabeledBox +2. TypeError OK +3. 42 +3. TypeError OK +4. temp: 98.6 +5. 42 - LabeledBox +5. class: LabeledBox +Done. diff --git a/Zend/tests/generics/generic_debug_display.phpt b/Zend/tests/generics/generic_debug_display.phpt new file mode 100644 index 0000000000000..77ff044e53c43 --- /dev/null +++ b/Zend/tests/generics/generic_debug_display.phpt @@ -0,0 +1,67 @@ +--TEST-- +Generic class: debug display with var_dump, print_r, and stack traces +--FILE-- + { + public function __construct(public T $value) {} + public function throwError(): void { + throw new RuntimeException("test"); + } +} + +class Pair { + public function __construct(public A $first, public B $second) {} +} + +// var_dump shows generic args +$b = new Box(42); +var_dump($b); + +// print_r shows generic args +echo "---\n"; +$p = new Pair("hello", 99); +print_r($p); + +// get_class returns raw class name (no generic args) +echo "---\n"; +echo get_class($b) . "\n"; +echo get_class($p) . "\n"; + +// Stack trace shows generic args +echo "---\n"; +try { + $b->throwError(); +} catch (Throwable $e) { + echo $e->getTraceAsString() . "\n"; +} + +// Inherited class with bound generic args +class IntBox extends Box {} +$ib = new IntBox(100); +var_dump($ib); + +echo "OK\n"; +?> +--EXPECTF-- +object(Box)#1 (1) { + ["value"]=> + int(42) +} +--- +Pair Object +( + [first] => hello + [second] => 99 +) +--- +Box +Pair +--- +#0 %s(%d): Box->throwError() +#1 {main} +object(IntBox)#%d (1) { + ["value"]=> + int(100) +} +OK diff --git a/Zend/tests/generics/generic_default_type.phpt b/Zend/tests/generics/generic_default_type.phpt new file mode 100644 index 0000000000000..a1e2f51b095e3 --- /dev/null +++ b/Zend/tests/generics/generic_default_type.phpt @@ -0,0 +1,90 @@ +--TEST-- +Generic class: default type parameters +--FILE-- + { + public function __construct(public T $value) {} + public function get(): T { return $this->value; } +} + +// With explicit type arg — overrides default +$c1 = new Container(42); +echo $c1->get() . "\n"; // 42 + +// Without type arg — uses default (string) +$c2 = new Container("hello"); +echo $c2->get() . "\n"; // hello + +// Using default via fewer args +$c3 = new Container("world"); +echo $c3->get() . "\n"; // world (inferred as string) + +// Multiple params with partial defaults +class Map { + /** @var array */ + private array $items = []; + + public function set(K $key, V $value): void { + $this->items[] = [$key, $value]; + } + + public function getLastValue(): V { + $last = end($this->items); + return $last[1]; + } +} + +// Provide both args +$m1 = new Map(); +$m1->set(1, "one"); +echo $m1->getLastValue() . "\n"; // one + +// Provide only K, V defaults to string +$m2 = new Map(); +$m2->set(1, "default_v"); +echo $m2->getLastValue() . "\n"; // default_v + +// Type enforcement with default +try { + $m3 = new Map(); + $m3->set(1, 42); // Should fail: V=string but got int +} catch (TypeError $e) { + echo "TypeError: V enforced\n"; +} + +// Default with constraint +class Repository { + public static function describe(): string { + return "Repository"; + } +} + +$r = new Repository(); +echo Repository::describe() . "\n"; + +// Reflection +$rc = new ReflectionClass('Map'); +$params = $rc->getGenericParameters(); +echo "Map params: " . count($params) . "\n"; +echo " K: hasDefault=" . var_export($params[0]->hasDefaultType(), true) . "\n"; +echo " V: hasDefault=" . var_export($params[1]->hasDefaultType(), true) . "\n"; +echo " V default: " . $params[1]->getDefaultType() . "\n"; + +echo "OK\n"; +?> +--EXPECTF-- +42 +hello +world +one +default_v +TypeError: V enforced +Repository +Map params: 2 + K: hasDefault=false + V: hasDefault=true + V default: string +OK diff --git a/Zend/tests/generics/generic_destructor.phpt b/Zend/tests/generics/generic_destructor.phpt new file mode 100644 index 0000000000000..5e510b562a96a --- /dev/null +++ b/Zend/tests/generics/generic_destructor.phpt @@ -0,0 +1,64 @@ +--TEST-- +Generic class: destructor runs with generic_args intact +--FILE-- + { + public T $value; + + public function __construct(T $value) { + $this->value = $value; + } + + public function __destruct() { + // generic_args should still be accessible during destruction + echo "destroying Box with value: " . $this->value . "\n"; + } +} + +// 1. Normal destruction at end of scope +function test_scope(): void { + $b = new Box(42); + echo "1. created\n"; +} +test_scope(); + +// 2. Explicit unset +$b = new Box("hello"); +echo "2. before unset\n"; +unset($b); +echo "2. after unset\n"; + +// 3. Replacement triggers destruction +$b = new Box(1); +echo "3. before replace\n"; +$b = new Box(2); +echo "3. after replace\n"; + +// 4. Destruction order in array +$arr = [ + new Box(10), + new Box(20), +]; +echo "4. before array unset\n"; +unset($arr); +echo "4. after array unset\n"; + +echo "Done.\n"; +?> +--EXPECT-- +1. created +destroying Box with value: 42 +2. before unset +destroying Box with value: hello +2. after unset +3. before replace +destroying Box with value: 1 +3. after replace +4. before array unset +destroying Box with value: 10 +destroying Box with value: 20 +4. after array unset +Done. +destroying Box with value: 2 diff --git a/Zend/tests/generics/generic_enum_interface.phpt b/Zend/tests/generics/generic_enum_interface.phpt new file mode 100644 index 0000000000000..3f5407cd66ba3 --- /dev/null +++ b/Zend/tests/generics/generic_enum_interface.phpt @@ -0,0 +1,50 @@ +--TEST-- +Generic class: enum implementing generic interface +--FILE-- + { + public function value(): T; +} + +// Backed enum implementing generic interface with bound type +enum Color: string implements Container { + case Red = 'red'; + case Blue = 'blue'; + case Green = 'green'; + + public function value(): string { + return $this->value; + } +} + +// 1. Basic usage +$c = Color::Red; +echo "1. " . $c->value() . "\n"; + +// 2. instanceof generic interface +echo "2. is Container: " . ($c instanceof Container ? "yes" : "no") . "\n"; + +// 3. Type hint accepting generic interface +function getFromContainer(Container $c): string { + return $c->value(); +} + +echo "3. " . getFromContainer(Color::Blue) . "\n"; + +// 4. All enum cases +foreach (Color::cases() as $case) { + echo "4. " . $case->name . "=" . $case->value() . "\n"; +} + +echo "Done.\n"; +?> +--EXPECT-- +1. red +2. is Container: yes +3. blue +4. Red=red +4. Blue=blue +4. Green=green +Done. diff --git a/Zend/tests/generics/generic_error_message_resolved.phpt b/Zend/tests/generics/generic_error_message_resolved.phpt new file mode 100644 index 0000000000000..9f8bc45fd2d91 --- /dev/null +++ b/Zend/tests/generics/generic_error_message_resolved.phpt @@ -0,0 +1,17 @@ +--TEST-- +Generic class: error messages show resolved type names +--FILE-- + { + public function __construct(T $value) {} +} + +// Should show "int" not "T" in error message +try { + $c = new Container("hello"); +} catch (TypeError $e) { + echo $e->getMessage() . "\n"; +} +?> +--EXPECTF-- +%s::__construct(): Argument #1 ($value) must be of type int, string given, called in %s on line %d diff --git a/Zend/tests/generics/generic_fibers.phpt b/Zend/tests/generics/generic_fibers.phpt new file mode 100644 index 0000000000000..3d96c0ee94f5b --- /dev/null +++ b/Zend/tests/generics/generic_fibers.phpt @@ -0,0 +1,47 @@ +--TEST-- +Generic class: generic objects work across fiber suspend/resume +--FILE-- + { + public function __construct(public T $value) {} + public function get(): T { return $this->value; } +} + +$fiber = new Fiber(function () { + $b = new Box(42); + echo "Fiber: " . $b->get() . "\n"; + + // Suspend and pass a generic object out + Fiber::suspend($b); + + // After resume, modify and check type enforcement + $b->value = 100; + echo "Fiber resumed: " . $b->get() . "\n"; + + try { + $b->value = "not an int"; + } catch (TypeError $e) { + echo "Fiber TypeError: type enforced\n"; + } +}); + +// Start the fiber and receive the generic object +$box = $fiber->start(); +echo "Main received: "; +var_dump($box); + +// Resume the fiber +$fiber->resume(); + +echo "OK\n"; +?> +--EXPECTF-- +Fiber: 42 +Main received: object(Box)#%d (1) { + ["value"]=> + int(42) +} +Fiber resumed: 100 +Fiber TypeError: type enforced +OK diff --git a/Zend/tests/generics/generic_file_cache.phpt b/Zend/tests/generics/generic_file_cache.phpt new file mode 100644 index 0000000000000..581eb0a61b0fc --- /dev/null +++ b/Zend/tests/generics/generic_file_cache.phpt @@ -0,0 +1,77 @@ +--TEST-- +Generic classes work with opcache file cache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache={TMP} +opcache.file_cache_only=1 +--FILE-- + { + private T $value; + public function __construct(T $value) { $this->value = $value; } + public function get(): T { return $this->value; } +} + +class Pair { + public function __construct(private K $key, private V $val) {} + public function getKey(): K { return $this->key; } + public function getVal(): V { return $this->val; } +} + +// Basic generic instantiation +$box = new Box(42); +echo $box->get() . "\n"; + +// Type enforcement +try { + $box2 = new Box("hello"); +} catch (TypeError $e) { + echo "TypeError: OK\n"; +} + +// Default type parameters +$pair = new Pair(42, "age"); +echo $pair->getKey() . "\n"; +echo $pair->getVal() . "\n"; + +// Inheritance with bound generic args +class IntBox extends Box {} +$ib = new IntBox(99); +echo $ib->get() . "\n"; + +// Wildcard types +function acceptAny(Box $b): void { + echo "Any: OK\n"; +} +acceptAny($box); +acceptAny(new Box("hi")); + +// Static method call with generics +class Factory { + public static function create(T $value): T { + return $value; + } +} +echo Factory::create(123) . "\n"; + +// var_dump display +var_dump($box); + +echo "Done.\n"; +?> +--EXPECTF-- +42 +TypeError: OK +42 +age +99 +Any: OK +Any: OK +123 +object(Box)#%d (1) { + ["value":"Box":private]=> + int(42) +} +Done. diff --git a/Zend/tests/generics/generic_forwarded_type_args.phpt b/Zend/tests/generics/generic_forwarded_type_args.phpt new file mode 100644 index 0000000000000..1601cb239b674 --- /dev/null +++ b/Zend/tests/generics/generic_forwarded_type_args.phpt @@ -0,0 +1,63 @@ +--TEST-- +Generic type argument forwarding (new Box inside generic methods) +--FILE-- + { + public T $value; + public function __construct(T $value) { $this->value = $value; } + public function get(): T { return $this->value; } +} + +// Static method forwarding type args +class Factory { + public static function create(T $val): Box { + return new Box($val); + } +} + +$box = Factory::create(42); +echo $box->get() . "\n"; + +$sbox = Factory::create("hello"); +echo $sbox->get() . "\n"; + +// Instance method forwarding type args +class Wrapper { + public function wrap(T $val): Box { + return new Box($val); + } +} + +$w = new Wrapper(); +$b = $w->wrap(99); +echo $b->get() . "\n"; + +// Type enforcement on forwarded object +try { + $box->value = "wrong"; +} catch (TypeError $e) { + echo "TypeError caught\n"; +} + +// Nested forwarding: Factory creates Wrapper which creates Box +class NestedFactory { + public static function makeWrapped(T $val): Box { + $w = new Wrapper(); + return $w->wrap($val); + } +} + +$nb = NestedFactory::makeWrapped(77); +echo $nb->get() . "\n"; + +echo "OK\n"; +?> +--EXPECT-- +42 +hello +99 +TypeError caught +77 +OK diff --git a/Zend/tests/generics/generic_func_inference_basic.phpt b/Zend/tests/generics/generic_func_inference_basic.phpt new file mode 100644 index 0000000000000..fd4e0b164c3b1 --- /dev/null +++ b/Zend/tests/generics/generic_func_inference_basic.phpt @@ -0,0 +1,92 @@ +--TEST-- +Generic function: lazy type inference from call arguments +--DESCRIPTION-- +Tests the lazy inference path in zend_check_type_slow() which infers +generic type params from call arguments on first type check. +Note: generic function inference only enforces the return type — it +does not retroactively validate other parameters with the same T. +--FILE-- +(T $v): T { + return $v; +} + +echo "1. " . identity(42) . "\n"; +echo "1. " . identity("hello") . "\n"; + +// 2. Wrong return type (array is not coercible to string) +function broken(T $v): T { + return [1, 2, 3]; +} + +try { + broken("hello"); + echo "FAIL\n"; +} catch (TypeError $e) { + echo "2. TypeError OK\n"; +} + +// 3. Multiple type params: infer A and B independently +function make_pair(A $a, B $b): array { + return [$a, $b]; +} + +$p = make_pair(42, "hello"); +echo "3. " . $p[0] . " " . $p[1] . "\n"; + +// 4. Type param only in return, can't infer — no check +function make(): T { + return 42; +} + +echo "4. " . make() . "\n"; // no error, T can't be inferred + +// 5. Param with constraint +function count_items(T $v): int { + return count($v); +} + +echo "5. " . count_items(new ArrayObject([1, 2, 3])) . "\n"; + +// 6. Boolean inference +function check(T $v): T { + return $v; +} + +echo "6. " . (check(true) ? "true" : "false") . "\n"; +echo "6. " . (check(false) ? "true" : "false") . "\n"; + +// 7. Object inference +class Foo { public string $name = "Foo"; } + +function get_name(T $obj): string { + if ($obj instanceof Foo) return $obj->name; + return "unknown"; +} + +echo "7. " . get_name(new Foo()) . "\n"; + +// 8. Null inference (IS_NULL) +function nullable(T $v): T { + return $v; +} + +var_dump(nullable(null)); + +echo "Done.\n"; +?> +--EXPECT-- +1. 42 +1. hello +2. TypeError OK +3. 42 hello +4. 42 +5. 3 +6. true +6. false +7. Foo +NULL +Done. diff --git a/Zend/tests/generics/generic_func_inference_failure.phpt b/Zend/tests/generics/generic_func_inference_failure.phpt new file mode 100644 index 0000000000000..4145a622d1cf8 --- /dev/null +++ b/Zend/tests/generics/generic_func_inference_failure.phpt @@ -0,0 +1,64 @@ +--TEST-- +Generic function: inference failure cases (all-or-nothing semantics) +--DESCRIPTION-- +Tests that if not all generic type params can be inferred from call +arguments, the inference fails entirely and types go unchecked. +--FILE-- +(): T { + return "hello"; +} + +// No TypeError even though T can't be inferred +echo "1. " . make_value() . "\n"; + +// 2. Two params, only one inferrable +function partial(A $a): B { + return $a; // B can't be inferred — no enforcement +} + +// No TypeError — B is not in any parameter +echo "2. " . partial(42) . "\n"; + +// 3. No args at all, but T in params (called with fewer args than declared) +// This would be a regular PHP error, not a generics error +function needs_arg(T $v): T { + return $v; +} + +try { + needs_arg(); // Too few arguments +} catch (\ArgumentCountError $e) { + echo "3. ArgumentCountError OK\n"; +} + +// 4. First param non-generic, second generic — inference works +function second_generic(int $a, T $b): T { + return $b; +} + +echo "4. " . second_generic(1, "hello") . "\n"; + +// With wrong return: +function bad_second(int $a, T $b): T { + return [1]; // array, not T +} + +try { + bad_second(1, "hello"); // T=string, returning array + echo "FAIL\n"; +} catch (TypeError $e) { + echo "4. TypeError OK\n"; +} + +echo "Done.\n"; +?> +--EXPECT-- +1. hello +2. 42 +3. ArgumentCountError OK +4. hello +4. TypeError OK +Done. diff --git a/Zend/tests/generics/generic_func_inference_objects.phpt b/Zend/tests/generics/generic_func_inference_objects.phpt new file mode 100644 index 0000000000000..871ff20c17ee4 --- /dev/null +++ b/Zend/tests/generics/generic_func_inference_objects.phpt @@ -0,0 +1,96 @@ +--TEST-- +Generic function: inference with object and class types +--DESCRIPTION-- +Tests generic function type inference when arguments are objects. +The inference extracts the class name from the zval and uses instanceof +to verify return type compatibility. +--FILE-- +name = $name; } +} +class Dog extends Animal {} +class Cat extends Animal {} + +// 1. Basic object inference +function get_name(T $obj): string { + if ($obj instanceof Animal) return $obj->name; + return "unknown"; +} + +echo "1. " . get_name(new Dog("Rex")) . "\n"; +echo "1. " . get_name(new Cat("Whiskers")) . "\n"; + +// 2. Object identity — infer T=Dog, enforce Dog return +function clone_animal(T $animal): T { + return $animal; // same object back +} + +$d = clone_animal(new Dog("Rex")); +echo "2. " . get_class($d) . "\n"; + +// 3. Wrong return type: returning sibling class (Cat is not Dog) +function wrong_clone(T $animal): T { + return new Cat("Wrong"); // Cat is not Dog +} + +try { + wrong_clone(new Dog("Rex")); // T=Dog, returning Cat + echo "FAIL\n"; +} catch (TypeError $e) { + echo "3. sibling TypeError OK\n"; +} + +// 4. Wrong return type: returning array instead of object +function bad_clone(T $animal): T { + return [1, 2, 3]; // array is never an object +} + +try { + bad_clone(new Dog("Rex")); // T=Dog, returning array + echo "FAIL\n"; +} catch (TypeError $e) { + echo "4. array TypeError OK\n"; +} + +// 5. Object + scalar mix +function describe(A $obj, B $label): string { + if ($obj instanceof Animal) return $obj->name . ": " . $label; + return "?: " . $label; +} + +echo "5. " . describe(new Dog("Rex"), "good boy") . "\n"; + +// 6. stdClass inference +function get_prop(T $obj): string { + if ($obj instanceof stdClass) return $obj->x ?? "none"; + return "not stdClass"; +} + +$o = new stdClass(); +$o->x = "hello"; +echo "6. " . get_prop($o) . "\n"; + +// 7. Returning subclass is OK (Dog is an Animal) +function upcast(T $animal): T { + return $animal; +} + +$dog = upcast(new Dog("Buddy")); +echo "7. " . get_class($dog) . ": " . $dog->name . "\n"; + +echo "Done.\n"; +?> +--EXPECT-- +1. Rex +1. Whiskers +2. Dog +3. sibling TypeError OK +4. array TypeError OK +5. Rex: good boy +6. hello +7. Dog: Buddy +Done. diff --git a/Zend/tests/generics/generic_func_inference_variadic.phpt b/Zend/tests/generics/generic_func_inference_variadic.phpt new file mode 100644 index 0000000000000..026ffbcfcbc1a --- /dev/null +++ b/Zend/tests/generics/generic_func_inference_variadic.phpt @@ -0,0 +1,69 @@ +--TEST-- +Generic function: inference from variadic parameters +--DESCRIPTION-- +Tests that generic type params can be inferred from variadic arguments. +The variadic args are relocated by zend_copy_extra_args() to +EX_VAR_NUM(last_var + T), so inference reads from there. +--FILE-- +(T ...$args): T { + return $args[0]; +} + +echo "1. " . first(1, 2, 3) . "\n"; +echo "1. " . first("a", "b") . "\n"; + +// 2. Wrong return type with variadic (array can't coerce to int) +function bad_first(T ...$args): T { + return [999]; +} + +try { + bad_first(42, 43); + echo "FAIL\n"; +} catch (TypeError $e) { + echo "2. TypeError OK\n"; +} + +// 3. Mixed regular + variadic params +function mixed_params(A $first, B ...$rest): A { + return $first; +} + +echo "3. " . mixed_params("hello", 1, 2, 3) . "\n"; + +// Wrong return: should return A(string), not B(int) +function bad_mixed(A $first, B ...$rest): A { + return $rest[0]; // returns B instead of A +} + +try { + bad_mixed("hello", [1]); // A=string, B=array + echo "FAIL\n"; +} catch (TypeError $e) { + echo "3. TypeError OK\n"; +} + +// 4. Variadic with no extra args — inference from regular param position +function variadic_empty(T ...$args): T { + return $args[0] ?? throw new RuntimeException("no args"); +} + +echo "4. " . variadic_empty(42) . "\n"; + +// 5. Single variadic arg +echo "5. " . first(3.14) . "\n"; + +echo "Done.\n"; +?> +--EXPECT-- +1. 1 +1. a +2. TypeError OK +3. hello +3. TypeError OK +4. 42 +5. 3.14 +Done. diff --git a/Zend/tests/generics/generic_function_basic.phpt b/Zend/tests/generics/generic_function_basic.phpt new file mode 100644 index 0000000000000..d4bd8628898ae --- /dev/null +++ b/Zend/tests/generics/generic_function_basic.phpt @@ -0,0 +1,16 @@ +--TEST-- +Generic function: basic declaration and invocation +--FILE-- +(T $value): T { + return $value; +} + +echo identity(42) . "\n"; +echo identity("hello") . "\n"; +echo identity(3.14) . "\n"; +?> +--EXPECT-- +42 +hello +3.14 diff --git a/Zend/tests/generics/generic_function_return_enforcement.phpt b/Zend/tests/generics/generic_function_return_enforcement.phpt new file mode 100644 index 0000000000000..602d00c6664d9 --- /dev/null +++ b/Zend/tests/generics/generic_function_return_enforcement.phpt @@ -0,0 +1,115 @@ +--TEST-- +Generic function: comprehensive return type enforcement +--DESCRIPTION-- +Tests various scenarios for generic function return type enforcement via +lazy inference from call arguments. Uses non-coercible type mismatches +to avoid PHP's weak type coercion (int→string is allowed in weak mode). +--FILE-- +(T $v): T { + return [1, 2, 3]; +} + +try { + broken("hello"); + echo "FAIL: expected TypeError\n"; +} catch (TypeError $e) { + echo "1. OK: caught TypeError\n"; +} + +// 2. Correct return type passes +function identity(T $v): T { + return $v; +} + +echo "2. OK: " . identity(42) . "\n"; +echo "2. OK: " . identity("hello") . "\n"; + +// 3. Multiple type params — returning wrong param type fails +function swap(A $a, B $b): A { + return $b; // returns B(array) instead of A(string) +} + +try { + swap("hello", [1]); + echo "FAIL: expected TypeError\n"; +} catch (TypeError $e) { + echo "3. OK: caught TypeError\n"; +} + +// 4. Type param only in return (not in params) — can't infer, remains unchecked +function make_int(): T { + return 42; +} + +echo "4. OK: " . make_int() . "\n"; // no TypeError — T can't be inferred + +// 5. Nested calls — each function infers independently +function outer(T $v): T { + return identity($v); // identity infers its own T +} + +echo "5. OK: " . outer(99) . "\n"; +echo "5. OK: " . outer("nested") . "\n"; + +// 6. Variadic — infer T from first variadic arg +function first(T ...$args): T { + return $args[0]; +} + +echo "6. OK: " . first(1, 2, 3) . "\n"; +echo "6. OK: " . first("a", "b") . "\n"; + +// 7. Variadic with wrong return type (array can't coerce to string) +function bad_first(T ...$args): T { + return [3.14]; +} + +try { + bad_first("a", "b"); + echo "FAIL: expected TypeError\n"; +} catch (TypeError $e) { + echo "7. OK: caught TypeError\n"; +} + +// 8. Correct return of same type +function passthrough(T $v): T { + return $v; +} + +echo "8. OK: " . passthrough(true) . "\n"; + +// 9. Int return for int param — no error +function double_it(T $v): T { + if (is_int($v)) return $v * 2; + return $v; +} + +echo "9. OK: " . double_it(5) . "\n"; +echo "9. OK: " . double_it("keep") . "\n"; + +// 10. Coercible types are allowed in weak mode (int→string is fine) +function weak_coerce(T $v): T { + return 42; // int can coerce to string in weak mode +} + +echo "10. OK: " . weak_coerce("hello") . "\n"; // passes due to int→string coercion + +?> +--EXPECT-- +1. OK: caught TypeError +2. OK: 42 +2. OK: hello +3. OK: caught TypeError +4. OK: 42 +5. OK: 99 +5. OK: nested +6. OK: 1 +6. OK: a +7. OK: caught TypeError +8. OK: 1 +9. OK: 10 +9. OK: keep +10. OK: 42 diff --git a/Zend/tests/generics/generic_function_return_jit.phpt b/Zend/tests/generics/generic_function_return_jit.phpt new file mode 100644 index 0000000000000..6b7748cc37b76 --- /dev/null +++ b/Zend/tests/generics/generic_function_return_jit.phpt @@ -0,0 +1,106 @@ +--TEST-- +Generic function return type enforcement with JIT +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=1255 +opcache.jit_buffer_size=64M +--FILE-- +(T $v): T { + return [1, 2]; +} + +try { + broken("hello"); + echo "FAIL\n"; +} catch (TypeError $e) { + echo "1. OK: caught TypeError\n"; +} + +// 2. Correct return passes +function identity(T $v): T { + return $v; +} + +echo "2. OK: " . identity(42) . "\n"; +echo "2. OK: " . identity("hello") . "\n"; + +// 3. Multiple type params +function swap(A $a, B $b): A { + return $b; +} + +try { + swap("hello", [1]); + echo "FAIL\n"; +} catch (TypeError $e) { + echo "3. OK: caught TypeError\n"; +} + +// 4. Hot loop — triggers JIT compilation of generic function +function increment(T $v): T { + return $v; +} + +for ($i = 0; $i < 200; $i++) { + increment($i); +} +echo "4. OK: hot loop int\n"; + +for ($i = 0; $i < 200; $i++) { + increment("str_$i"); +} +echo "4. OK: hot loop string\n"; + +// 5. Hot loop with enforcement check after JIT +function wrong_after_jit(T $v): T { + return [999]; +} + +// First make it hot with a type that fails non-coercibly +$caught = 0; +for ($i = 0; $i < 200; $i++) { + try { + wrong_after_jit("test"); + } catch (TypeError $e) { + $caught++; + } +} +echo "5. OK: caught $caught TypeErrors in hot loop\n"; + +// 6. Variadic with JIT +function first(T ...$args): T { + return $args[0]; +} + +for ($i = 0; $i < 200; $i++) { + first($i, $i + 1, $i + 2); +} +echo "6. OK: variadic hot loop\n"; + +// 7. Nested generic calls under JIT +function outer(T $v): T { + return identity($v); +} + +for ($i = 0; $i < 200; $i++) { + outer($i); +} +echo "7. OK: nested hot loop\n"; + +echo "Done.\n"; +?> +--EXPECT-- +1. OK: caught TypeError +2. OK: 42 +2. OK: hello +3. OK: caught TypeError +4. OK: hot loop int +4. OK: hot loop string +5. OK: caught 200 TypeErrors in hot loop +6. OK: variadic hot loop +7. OK: nested hot loop +Done. diff --git a/Zend/tests/generics/generic_function_return_opcache.phpt b/Zend/tests/generics/generic_function_return_opcache.phpt new file mode 100644 index 0000000000000..f649c2e351a7e --- /dev/null +++ b/Zend/tests/generics/generic_function_return_opcache.phpt @@ -0,0 +1,88 @@ +--TEST-- +Generic function return type enforcement with opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +--FILE-- +(T $v): T { + return [1, 2]; +} + +try { + broken("hello"); + echo "FAIL\n"; +} catch (TypeError $e) { + echo "1. OK: caught TypeError\n"; +} + +// 2. Correct return passes +function identity(T $v): T { + return $v; +} + +echo "2. OK: " . identity(42) . "\n"; +echo "2. OK: " . identity("hello") . "\n"; + +// 3. Multiple type params +function swap(A $a, B $b): A { + return $b; +} + +try { + swap("hello", [1]); + echo "FAIL\n"; +} catch (TypeError $e) { + echo "3. OK: caught TypeError\n"; +} + +// 4. Uninferrable type param remains unchecked +function make(): T { + return 42; +} + +echo "4. OK: " . make() . "\n"; + +// 5. Hot loop to trigger optimizer passes +function add_one(T $v): T { + return $v; +} + +for ($i = 0; $i < 100; $i++) { + add_one($i); +} +echo "5. OK: hot loop\n"; + +// 6. Variadic +function first(T ...$args): T { + return $args[0]; +} + +echo "6. OK: " . first("a", "b", "c") . "\n"; + +// 7. Variadic with wrong return +function bad_variadic(T ...$args): T { + return [1]; +} + +try { + bad_variadic("a", "b"); + echo "FAIL\n"; +} catch (TypeError $e) { + echo "7. OK: caught TypeError\n"; +} + +echo "Done.\n"; +?> +--EXPECT-- +1. OK: caught TypeError +2. OK: 42 +2. OK: hello +3. OK: caught TypeError +4. OK: 42 +5. OK: hot loop +6. OK: a +7. OK: caught TypeError +Done. diff --git a/Zend/tests/generics/generic_function_return_type.phpt b/Zend/tests/generics/generic_function_return_type.phpt new file mode 100644 index 0000000000000..ec22bbc5486ab --- /dev/null +++ b/Zend/tests/generics/generic_function_return_type.phpt @@ -0,0 +1,68 @@ +--TEST-- +Generic function: return type enforcement +--DESCRIPTION-- +Generic functions infer T from arguments and enforce return type at runtime. +If a generic function declares return type T but returns a value of a different +type than what T was inferred to be, a TypeError is thrown. +Note: PHP's standard weak type coercion still applies (e.g., int→string is OK +in weak mode), so tests use non-coercible type mismatches. +--FILE-- +(T $value): T { + return $value; +} + +echo identity(42) . "\n"; +echo identity("hello") . "\n"; + +// 2. Function that returns wrong type (array instead of string — not coercible) +function broken_identity(T $value): T { + return [1, 2, 3]; // returns array regardless of T +} + +try { + $result = broken_identity("hello"); // T=string inferred, but returns array + echo "BUG: no TypeError for wrong return type\n"; +} catch (TypeError $e) { + echo "OK: caught TypeError\n"; +} + +// 3. Nested calls — return type should propagate through call chain +function wrap_and_return(T $value): T { + return identity($value); +} + +echo wrap_and_return(42) . "\n"; +echo wrap_and_return("test") . "\n"; + +// 4. Multiple type params — each should be checked independently +function swap_first(A $a, B $b): A { + return $b; // wrong! returns B(array) instead of A(string) +} + +try { + $result = swap_first("hello", [1, 2]); // A=string, B=array, returns array + echo "BUG: no TypeError for returning B where A expected\n"; +} catch (TypeError $e) { + echo "OK: caught TypeError\n"; +} + +// 5. Parameter type checking DOES work (verify it) +function strict_param(T $a, T $b): T { + return $a; +} + +echo strict_param(1, 2) . "\n"; +echo strict_param("a", "b") . "\n"; +?> +--EXPECT-- +42 +hello +OK: caught TypeError +42 +test +OK: caught TypeError +1 +a diff --git a/Zend/tests/generics/generic_inheritance_arg_count_mismatch.phpt b/Zend/tests/generics/generic_inheritance_arg_count_mismatch.phpt new file mode 100644 index 0000000000000..735f9710d726a --- /dev/null +++ b/Zend/tests/generics/generic_inheritance_arg_count_mismatch.phpt @@ -0,0 +1,13 @@ +--TEST-- +Generic class: inheritance with wrong number of type arguments +--FILE-- + { + private $value; + public function __construct(T $value) { $this->value = $value; } +} + +class BadBox extends Box {} +?> +--EXPECTF-- +Fatal error: Class Box expects 1 generic type argument(s), 2 given in BadBox in %s on line %d diff --git a/Zend/tests/generics/generic_inheritance_method_resolution.phpt b/Zend/tests/generics/generic_inheritance_method_resolution.phpt new file mode 100644 index 0000000000000..0429a12bfd13d --- /dev/null +++ b/Zend/tests/generics/generic_inheritance_method_resolution.phpt @@ -0,0 +1,30 @@ +--TEST-- +Generic class: method signature resolution in inheritance +--FILE-- + { + private array $items = []; + public function add(T $item): void { $this->items[] = $item; } + public function first(): T { return $this->items[0]; } +} + +// Override with resolved concrete types — should be compatible +class IntList extends Collection { + public function add(int $item): void { + echo "IntList::add($item)\n"; + parent::add($item); + } + public function first(): int { + return parent::first(); + } +} + +$list = new IntList(); +$list->add(42); +echo $list->first() . "\n"; +echo "OK\n"; +?> +--EXPECT-- +IntList::add(42) +42 +OK diff --git a/Zend/tests/generics/generic_instanceof.phpt b/Zend/tests/generics/generic_instanceof.phpt new file mode 100644 index 0000000000000..fb7c95baea7f4 --- /dev/null +++ b/Zend/tests/generics/generic_instanceof.phpt @@ -0,0 +1,36 @@ +--TEST-- +Generic class: instanceof with generic type arguments +--FILE-- + { + public T $value; + public function __construct(T $value) { $this->value = $value; } +} + +$intBox = new Box(42); +$strBox = new Box("hello"); + +// instanceof without generic args (erasure) — always true for correct base class +var_dump($intBox instanceof Box); // true +var_dump($strBox instanceof Box); // true + +// instanceof with matching generic args +var_dump($intBox instanceof Box); // true +var_dump($strBox instanceof Box); // true + +// instanceof with non-matching generic args +var_dump($intBox instanceof Box); // false +var_dump($strBox instanceof Box); // false + +echo "OK\n"; +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +OK diff --git a/Zend/tests/generics/generic_interface_bad_override.phpt b/Zend/tests/generics/generic_interface_bad_override.phpt new file mode 100644 index 0000000000000..864cabaa0d4e6 --- /dev/null +++ b/Zend/tests/generics/generic_interface_bad_override.phpt @@ -0,0 +1,15 @@ +--TEST-- +Generic interface: incompatible implementation detected +--FILE-- + { + public function transform(T $input): T; +} + +// string is not compatible with int for Transformer +class BadTransformer implements Transformer { + public function transform(string $input): string { return $input; } +} +?> +--EXPECTF-- +Fatal error: Declaration of BadTransformer::transform(string $input): string must be compatible with Transformer::transform(T $input): T in %s on line %d diff --git a/Zend/tests/generics/generic_interface_basic.phpt b/Zend/tests/generics/generic_interface_basic.phpt new file mode 100644 index 0000000000000..c8eb6ce5de467 --- /dev/null +++ b/Zend/tests/generics/generic_interface_basic.phpt @@ -0,0 +1,13 @@ +--TEST-- +Generic interface: basic declaration +--FILE-- + { + public function add(T $item): void; + public function get(int $index): T; +} + +echo "Collection interface declared\n"; +?> +--EXPECT-- +Collection interface declared diff --git a/Zend/tests/generics/generic_interface_implementation.phpt b/Zend/tests/generics/generic_interface_implementation.phpt new file mode 100644 index 0000000000000..90781e0aa11fd --- /dev/null +++ b/Zend/tests/generics/generic_interface_implementation.phpt @@ -0,0 +1,27 @@ +--TEST-- +Generic interface: implementation with bound type arguments +--FILE-- + { + public function find(int $id): T; + public function save(T $entity): void; +} + +class UserRepo implements Repository { + private array $store = []; + public function find(int $id): string { return $this->store[$id] ?? "unknown"; } + public function save(string $entity): void { + $this->store[] = $entity; + echo "Saved: $entity\n"; + } +} + +$repo = new UserRepo(); +$repo->save("alice"); +echo $repo->find(0) . "\n"; +echo "OK\n"; +?> +--EXPECT-- +Saved: alice +alice +OK diff --git a/Zend/tests/generics/generic_intersection_constraint.phpt b/Zend/tests/generics/generic_intersection_constraint.phpt new file mode 100644 index 0000000000000..690d2e5f7ba6a --- /dev/null +++ b/Zend/tests/generics/generic_intersection_constraint.phpt @@ -0,0 +1,78 @@ +--TEST-- +Generic class: constraint with class type bound (valid usage) +--DESCRIPTION-- +Tests that generic type parameter constraints properly enforce that +type arguments implement the required interface. This test covers +valid constraint satisfaction. Constraint violations trigger E_ERROR +which is tested separately. +--FILE-- +val; } +} + +class PrintableString implements Printable { + public function __construct(private string $val) {} + public function toString(): string { return $this->val; } +} + +// Generic class with constraint +class Printer { + private T $item; + public function __construct(T $item) { $this->item = $item; } + public function print(): string { return $this->item->toString(); } +} + +// 1. Valid constraint satisfaction +$p1 = new Printer(new PrintableInt(42)); +echo "1. " . $p1->print() . "\n"; + +$p2 = new Printer(new PrintableString("hello")); +echo "1. " . $p2->print() . "\n"; + +// 2. Generic function with constraint +function stringify(T $item): string { + return $item->toString(); +} + +echo "2. " . stringify(new PrintableInt(99)) . "\n"; +echo "2. " . stringify(new PrintableString("world")) . "\n"; + +// 3. Inheritance with constrained parent +class SpecialPrinter extends Printer { + public function printDouble(): string { + return $this->print() . $this->print(); + } +} + +$sp = new SpecialPrinter(new PrintableInt(5)); +echo "3. " . $sp->printDouble() . "\n"; + +// 4. Multiple constrained instances +$printers = []; +for ($i = 0; $i < 5; $i++) { + $printers[] = new Printer(new PrintableInt($i)); +} +$parts = []; +foreach ($printers as $p) { + $parts[] = $p->print(); +} +echo "4. " . implode(" ", $parts) . "\n"; + +echo "Done.\n"; +?> +--EXPECT-- +1. 42 +1. hello +2. 99 +2. world +3. 55 +4. 0 1 2 3 4 +Done. diff --git a/Zend/tests/generics/generic_jit.phpt b/Zend/tests/generics/generic_jit.phpt new file mode 100644 index 0000000000000..3ccb5d60749ae --- /dev/null +++ b/Zend/tests/generics/generic_jit.phpt @@ -0,0 +1,58 @@ +--TEST-- +Generic classes work with JIT enabled +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=1255 +opcache.jit_buffer_size=64M +--FILE-- + { + private T $value; + public function __construct(T $value) { $this->value = $value; } + public function get(): T { return $this->value; } +} + +// Basic generic instantiation +$box = new Box(42); +echo $box->get() . "\n"; + +// Type enforcement +try { + $box2 = new Box("hello"); +} catch (TypeError $e) { + echo "TypeError: OK\n"; +} + +// Return type enforcement +class Container { + private T $item; + public function __construct(T $item) { $this->item = $item; } + public function getItem(): T { return $this->item; } +} + +$c = new Container("world"); +echo $c->getItem() . "\n"; + +// Multiple calls to trigger JIT compilation +for ($i = 0; $i < 100; $i++) { + $b = new Box($i); + $b->get(); +} +echo "Hot loop: OK\n"; + +// Inheritance with bound generic args +class IntBox extends Box {} +$ib = new IntBox(99); +echo $ib->get() . "\n"; + +echo "Done.\n"; +?> +--EXPECTF-- +42 +TypeError: OK +world +Hot loop: OK +99 +Done. diff --git a/Zend/tests/generics/generic_jit_ctor_inference.phpt b/Zend/tests/generics/generic_jit_ctor_inference.phpt new file mode 100644 index 0000000000000..1bfbe42773553 --- /dev/null +++ b/Zend/tests/generics/generic_jit_ctor_inference.phpt @@ -0,0 +1,75 @@ +--TEST-- +Generic class: JIT + constructor type inference with --repeat +--DESCRIPTION-- +Tests that constructor generic arg inference works correctly when the +constructor is JIT-compiled. The --repeat 2 flag re-executes the script, +and jit_hot_func=1 ensures JIT compilation after the first call. +This caught a real bug where JIT leave helpers didn't call +zend_infer_generic_args_from_constructor(). +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=tracing +opcache.jit_buffer_size=64M +opcache.jit_hot_func=1 +--FILE-- + { + private T $value; + public function __construct(T $value) { $this->value = $value; } + public function get(): T { return $this->value; } + public function set(T $value): void { $this->value = $value; } +} + +// Infer T=int +$box = new Box(42); +echo $box->get() . "\n"; + +// Must throw TypeError: T=int, passing string +try { + $box->set("hello"); +} catch (TypeError $e) { + echo "int box: TypeError OK\n"; +} + +// Infer T=string +$sbox = new Box("world"); +echo $sbox->get() . "\n"; + +// Must throw TypeError: T=string, passing array (non-coercible) +try { + $sbox->set([1, 2, 3]); +} catch (TypeError $e) { + echo "string box: TypeError OK\n"; +} + +// Infer T=float +$fbox = new Box(3.14); +echo $fbox->get() . "\n"; + +// Hot loop to trigger JIT compilation of constructor +for ($i = 0; $i < 100; $i++) { + $tmp = new Box($i); + $tmp->get(); +} + +// After JIT compilation, inference must still work +$postjit = new Box("after-jit"); +try { + $postjit->set([1]); // array can't coerce to string +} catch (TypeError $e) { + echo "post-jit: TypeError OK\n"; +} + +echo "Done.\n"; +?> +--EXPECT-- +42 +int box: TypeError OK +world +string box: TypeError OK +3.14 +post-jit: TypeError OK +Done. diff --git a/Zend/tests/generics/generic_jit_func_inference.phpt b/Zend/tests/generics/generic_jit_func_inference.phpt new file mode 100644 index 0000000000000..b48d31187a8c5 --- /dev/null +++ b/Zend/tests/generics/generic_jit_func_inference.phpt @@ -0,0 +1,87 @@ +--TEST-- +Generic function: JIT + function type inference +--DESCRIPTION-- +Tests that generic function argument inference and return type +enforcement work correctly under JIT compilation. +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=tracing +opcache.jit_buffer_size=64M +opcache.jit_hot_func=1 +--FILE-- +(T $v): T { + return $v; +} + +echo "1. " . identity(42) . "\n"; +echo "1. " . identity("hello") . "\n"; + +// 2. Wrong return type (array is not coercible to string) +function broken(T $v): T { + return [1, 2]; +} + +try { + broken("test"); + echo "FAIL\n"; +} catch (TypeError $e) { + echo "2. TypeError OK\n"; +} + +// 3. Hot loop — trigger JIT compilation +for ($i = 0; $i < 200; $i++) { + identity($i); +} +echo "3. hot loop int OK\n"; + +for ($i = 0; $i < 200; $i++) { + identity("str_$i"); +} +echo "3. hot loop string OK\n"; + +// 4. After JIT, enforcement must still work +try { + broken("after-jit"); + echo "FAIL\n"; +} catch (TypeError $e) { + echo "4. post-jit TypeError OK\n"; +} + +// 5. Multiple type params under JIT +function pair(A $a, B $b): A { + return $a; +} + +for ($i = 0; $i < 200; $i++) { + pair($i, "x"); +} +echo "5. multi-param hot OK\n"; + +// Wrong return for multi-param +function bad_pair(A $a, B $b): A { + return $b; // returns B instead of A +} + +try { + bad_pair("hello", [1]); // A=string, B=array, returning array + echo "FAIL\n"; +} catch (TypeError $e) { + echo "5. multi-param TypeError OK\n"; +} + +echo "Done.\n"; +?> +--EXPECT-- +1. 42 +1. hello +2. TypeError OK +3. hot loop int OK +3. hot loop string OK +4. post-jit TypeError OK +5. multi-param hot OK +5. multi-param TypeError OK +Done. diff --git a/Zend/tests/generics/generic_jit_nested.phpt b/Zend/tests/generics/generic_jit_nested.phpt new file mode 100644 index 0000000000000..7d051600b4672 --- /dev/null +++ b/Zend/tests/generics/generic_jit_nested.phpt @@ -0,0 +1,62 @@ +--TEST-- +Generic class: JIT + nested generic objects +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=tracing +opcache.jit_buffer_size=64M +--FILE-- + { + public T $value; + public function __construct(T $value) { $this->value = $value; } + public function get(): T { return $this->value; } + public function set(T $value): void { $this->value = $value; } +} + +// 1. Nested Box> +$inner = new Box(42); +$outer = new Box>($inner); +echo "1. " . $outer->get()->get() . "\n"; + +// 2. Hot loop with nested generics +for ($i = 0; $i < 200; $i++) { + $b = new Box($i); + $o = new Box>($b); + $o->get()->get(); +} +echo "2. hot nested OK\n"; + +// 3. Type error on inner type after JIT +$ibox = new Box(10); +$obox = new Box>($ibox); +try { + $obox->get()->set("wrong"); // inner T=int, passing string +} catch (TypeError $e) { + echo "3. inner TypeError OK\n"; +} + +// 4. Type error on outer type after JIT +try { + $obox->set("not a box"); // outer T=Box, passing string +} catch (TypeError $e) { + echo "4. outer TypeError OK\n"; +} + +// 5. Three levels deep +$l1 = new Box(7); +$l2 = new Box>($l1); +$l3 = new Box>>($l2); +echo "5. " . $l3->get()->get()->get() . "\n"; + +echo "Done.\n"; +?> +--EXPECT-- +1. 42 +2. hot nested OK +3. inner TypeError OK +4. outer TypeError OK +5. 7 +Done. diff --git a/Zend/tests/generics/generic_jit_property.phpt b/Zend/tests/generics/generic_jit_property.phpt new file mode 100644 index 0000000000000..35343d44b1d18 --- /dev/null +++ b/Zend/tests/generics/generic_jit_property.phpt @@ -0,0 +1,73 @@ +--TEST-- +Generic class: JIT + typed property assignment enforcement +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=tracing +opcache.jit_buffer_size=64M +opcache.jit_hot_func=1 +--FILE-- + { + public T $value; + public function __construct(T $value) { $this->value = $value; } +} + +// 1. Basic property access +$c = new Container(42); +echo "1. " . $c->value . "\n"; + +// 2. Valid property assignment +$c->value = 99; +echo "2. " . $c->value . "\n"; + +// 3. Invalid property assignment +try { + $c->value = "hello"; +} catch (TypeError $e) { + echo "3. TypeError OK\n"; +} + +// 4. Hot loop — property assignment under JIT +$box = new Container(0); +for ($i = 0; $i < 1000; $i++) { + $box->value = $i; +} +echo "4. hot prop: " . $box->value . "\n"; + +// 5. After JIT, type errors still caught on property +$box2 = new Container("init"); +for ($i = 0; $i < 200; $i++) { + $box2->value = "val$i"; +} + +try { + $box2->value = [1, 2]; // array can't coerce to string +} catch (TypeError $e) { + echo "5. post-jit prop TypeError OK\n"; +} + +// 6. Inferred type — property enforcement +$inferred = new Container(3.14); +for ($i = 0; $i < 200; $i++) { + $inferred->value = (float)$i; +} + +try { + $inferred->value = "not a float"; +} catch (TypeError $e) { + echo "6. inferred prop TypeError OK\n"; +} + +echo "Done.\n"; +?> +--EXPECT-- +1. 42 +2. 99 +3. TypeError OK +4. hot prop: 999 +5. post-jit prop TypeError OK +6. inferred prop TypeError OK +Done. diff --git a/Zend/tests/generics/generic_jit_variance.phpt b/Zend/tests/generics/generic_jit_variance.phpt new file mode 100644 index 0000000000000..b6069816dff19 --- /dev/null +++ b/Zend/tests/generics/generic_jit_variance.phpt @@ -0,0 +1,67 @@ +--TEST-- +Generic class: JIT + covariant/contravariant type checks +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=tracing +opcache.jit_buffer_size=64M +--FILE-- + { + private mixed $value; + public function __construct(mixed $value) { $this->value = $value; } + public function get(): T { return $this->value; } +} + +// Contravariant: in T — only in parameter positions +class WriteOnlyBox { + private mixed $value; + public function set(T $value): void { $this->value = $value; } + public function dump(): void { echo get_class($this->value) . "\n"; } +} + +// 1. Covariant basic +$dogBox = new ReadOnlyBox(new Dog()); +echo "1. " . get_class($dogBox->get()) . "\n"; + +// 2. Contravariant basic +$animalBox = new WriteOnlyBox(null); +$animalBox->set(new Dog()); +echo "2. OK\n"; +$animalBox->dump(); + +// 3. Hot loop with covariant +for ($i = 0; $i < 200; $i++) { + $rb = new ReadOnlyBox(new Dog()); + $rb->get(); +} +echo "3. hot covariant OK\n"; + +// 4. Hot loop with contravariant +for ($i = 0; $i < 200; $i++) { + $wb = new WriteOnlyBox(null); + $wb->set(new Cat()); +} +echo "4. hot contravariant OK\n"; + +// 5. instanceof with covariant +$rb = new ReadOnlyBox(new Dog()); +echo "5. instanceof: " . ($rb instanceof ReadOnlyBox ? "yes" : "no") . "\n"; + +echo "Done.\n"; +?> +--EXPECT-- +1. Dog +2. OK +Dog +3. hot covariant OK +4. hot contravariant OK +5. instanceof: yes +Done. diff --git a/Zend/tests/generics/generic_multi_interface.phpt b/Zend/tests/generics/generic_multi_interface.phpt new file mode 100644 index 0000000000000..ae3ff2d6c0bb3 --- /dev/null +++ b/Zend/tests/generics/generic_multi_interface.phpt @@ -0,0 +1,69 @@ +--TEST-- +Generic class: implementing multiple generic interfaces +--FILE-- + { + public function read(): T; +} + +interface Writable { + public function write(T $value): void; +} + +interface Transformable { + public function transform(T $input): U; +} + +// Implements two generic interfaces with same type +class Storage implements Readable, Writable { + private T $data; + public function __construct(T $initial) { $this->data = $initial; } + public function read(): T { return $this->data; } + public function write(T $value): void { $this->data = $value; } +} + +// Implements two generic interfaces with bound types +class StringToIntConverter implements Readable, Transformable { + private string $data = ""; + public function read(): string { return $this->data; } + public function transform(string $input): int { return strlen($input); } +} + +// 1. Storage with same type for both interfaces +$s = new Storage(42); +echo "1. read: " . $s->read() . "\n"; +$s->write(99); +echo "1. after write: " . $s->read() . "\n"; + +try { + $s->write("not int"); +} catch (TypeError $e) { + echo "1. TypeError OK\n"; +} + +// 2. Bound types +$conv = new StringToIntConverter(); +echo "2. transform: " . $conv->transform("hello") . "\n"; +echo "2. read: '" . $conv->read() . "'\n"; + +// 3. instanceof checks +echo "3. Storage is Readable: " . ($s instanceof Readable ? "yes" : "no") . "\n"; +echo "3. Storage is Writable: " . ($s instanceof Writable ? "yes" : "no") . "\n"; +echo "3. Converter is Readable: " . ($conv instanceof Readable ? "yes" : "no") . "\n"; +echo "3. Converter is Transformable: " . ($conv instanceof Transformable ? "yes" : "no") . "\n"; + +echo "Done.\n"; +?> +--EXPECT-- +1. read: 42 +1. after write: 99 +1. TypeError OK +2. transform: 5 +2. read: '' +3. Storage is Readable: yes +3. Storage is Writable: yes +3. Converter is Readable: yes +3. Converter is Transformable: yes +Done. diff --git a/Zend/tests/generics/generic_multilevel_inheritance.phpt b/Zend/tests/generics/generic_multilevel_inheritance.phpt new file mode 100644 index 0000000000000..245243f4c6a23 --- /dev/null +++ b/Zend/tests/generics/generic_multilevel_inheritance.phpt @@ -0,0 +1,70 @@ +--TEST-- +Generic class: multi-level inheritance with bound type arguments +--FILE-- + -> B extends A -> C extends B +class Collection { + protected array $items = []; + public function add(T $item): void { $this->items[] = $item; } + public function first(): T { return $this->items[0]; } + public function count(): int { return count($this->items); } +} + +class SortableCollection extends Collection { + public function sort(): void { sort($this->items); } +} + +class IntSortableCollection extends SortableCollection {} + +// 1. Three-level chain works +$c = new IntSortableCollection(); +$c->add(3); +$c->add(1); +$c->add(2); +$c->sort(); +echo "1. first: " . $c->first() . ", count: " . $c->count() . "\n"; + +// 2. Type enforcement at bottom level +try { + $c->add("not an int"); +} catch (TypeError $e) { + echo "2. TypeError OK\n"; +} + +// 3. Middle level with explicit args +$sc = new SortableCollection(); +$sc->add("banana"); +$sc->add("apple"); +$sc->sort(); +echo "3. first: " . $sc->first() . "\n"; + +try { + $sc->add(42); +} catch (TypeError $e) { + echo "3. TypeError OK\n"; +} + +// 4. Direct instantiation with explicit type +$typed = new Collection(); +$typed->add(1.1); +$typed->add(2.2); +echo "4. first: " . $typed->first() . ", count: " . $typed->count() . "\n"; + +try { + $typed->add("bad"); +} catch (TypeError $e) { + echo "4. TypeError OK\n"; +} + +echo "Done.\n"; +?> +--EXPECT-- +1. first: 1, count: 3 +2. TypeError OK +3. first: apple +3. TypeError OK +4. first: 1.1, count: 2 +4. TypeError OK +Done. diff --git a/Zend/tests/generics/generic_nested.phpt b/Zend/tests/generics/generic_nested.phpt new file mode 100644 index 0000000000000..819badf344b7b --- /dev/null +++ b/Zend/tests/generics/generic_nested.phpt @@ -0,0 +1,39 @@ +--TEST-- +Generic class: nested generics with >> parsing and type enforcement +--FILE-- + { + public T $value; + public function __construct(T $value) { $this->value = $value; } + public function get(): T { return $this->value; } +} + +// Nested generics: Box> — tests >> parsing +$inner = new Box(42); +$outer = new Box>($inner); + +echo $outer->get()->get() . "\n"; + +// Wrong type on outer (expects Box, got string) +try { + $outer->value = "hello"; +} catch (TypeError $e) { + echo "outer: caught\n"; +} + +// Wrong type on inner (expects int, got string) +try { + $outer->get()->value = "hello"; +} catch (TypeError $e) { + echo "inner: caught\n"; +} + +echo "OK\n"; +?> +--EXPECT-- +42 +outer: caught +inner: caught +OK diff --git a/Zend/tests/generics/generic_objects_in_arrays.phpt b/Zend/tests/generics/generic_objects_in_arrays.phpt new file mode 100644 index 0000000000000..a97e9935b7911 --- /dev/null +++ b/Zend/tests/generics/generic_objects_in_arrays.phpt @@ -0,0 +1,74 @@ +--TEST-- +Generic class: generic objects stored in arrays and GC behavior +--FILE-- + { + public T $value; + public function __construct(T $value) { $this->value = $value; } +} + +// 1. Array of generic objects +$boxes = []; +for ($i = 0; $i < 5; $i++) { + $boxes[] = new Box($i); +} + +echo "1. count: " . count($boxes) . "\n"; +echo "1. first: " . $boxes[0]->value . "\n"; +echo "1. last: " . $boxes[4]->value . "\n"; + +// 2. Mixed generic types in array +$mixed = [ + new Box(42), + new Box("hello"), + new Box(3.14), +]; + +echo "2. int: " . $mixed[0]->value . "\n"; +echo "2. str: " . $mixed[1]->value . "\n"; +echo "2. float: " . $mixed[2]->value . "\n"; + +// 3. Unset — should free generic_args properly +unset($boxes[0]); +unset($boxes[1]); +echo "3. after unset, count: " . count($boxes) . "\n"; + +// 4. Array functions work +$mapped = array_map(fn(Box $b) => $b->value * 2, array_values(array_filter($boxes))); +echo "4. mapped: " . implode(", ", $mapped) . "\n"; + +// 5. Replace elements +$boxes[0] = new Box(100); +echo "5. replaced: " . $boxes[0]->value . "\n"; + +// 6. Nested array of generics +$nested = [ + [new Box(1), new Box(2)], + [new Box("a"), new Box("b")], +]; +echo "6. nested: " . $nested[0][0]->value . ", " . $nested[1][1]->value . "\n"; + +// 7. Type enforcement still works after array storage +try { + $mixed[0]->value = "not int"; +} catch (TypeError $e) { + echo "7. TypeError OK\n"; +} + +echo "Done.\n"; +?> +--EXPECT-- +1. count: 5 +1. first: 0 +1. last: 4 +2. int: 42 +2. str: hello +2. float: 3.14 +3. after unset, count: 3 +4. mapped: 4, 6, 8 +5. replaced: 100 +6. nested: 1, b +7. TypeError OK +Done. diff --git a/Zend/tests/generics/generic_opcache.phpt b/Zend/tests/generics/generic_opcache.phpt new file mode 100644 index 0000000000000..a04c3aa7d6a29 --- /dev/null +++ b/Zend/tests/generics/generic_opcache.phpt @@ -0,0 +1,66 @@ +--TEST-- +Generic classes work with opcache enabled +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +--FILE-- + { + private T $value; + public function __construct(T $value) { $this->value = $value; } + public function get(): T { return $this->value; } +} + +class Pair { + public function __construct(private K $key, private V $val) {} + public function getKey(): K { return $this->key; } + public function getVal(): V { return $this->val; } +} + +// Basic generic instantiation +$box = new Box(42); +echo $box->get() . "\n"; + +// Type enforcement +try { + $box2 = new Box("hello"); +} catch (TypeError $e) { + echo "TypeError: OK\n"; +} + +// Default type parameters +$pair = new Pair(42, "age"); +echo $pair->getKey() . "\n"; +echo $pair->getVal() . "\n"; + +// Inheritance with bound generic args +class IntBox extends Box {} +$ib = new IntBox(99); +echo $ib->get() . "\n"; + +// Wildcard types +function acceptAny(Box $b): void { + echo "Any: OK\n"; +} +acceptAny($box); +acceptAny(new Box("hi")); + +// var_dump display +var_dump($box); + +echo "Done.\n"; +?> +--EXPECTF-- +42 +TypeError: OK +42 +age +99 +Any: OK +Any: OK +object(Box)#%d (1) { + ["value":"Box":private]=> + int(42) +} +Done. diff --git a/Zend/tests/generics/generic_opcache_inheritance.phpt b/Zend/tests/generics/generic_opcache_inheritance.phpt new file mode 100644 index 0000000000000..e7eabd57a8b6d --- /dev/null +++ b/Zend/tests/generics/generic_opcache_inheritance.phpt @@ -0,0 +1,87 @@ +--TEST-- +Generic class: opcache with inheritance and bound type arguments +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +--FILE-- + { + protected array $items = []; + public function add(T $item): void { $this->items[] = $item; } + public function first(): T { return $this->items[0]; } + public function count(): int { return count($this->items); } +} + +// Child with bound type +class IntCollection extends Collection {} + +// Grandchild adding new methods (not overriding) +class PositiveIntCollection extends IntCollection { + public function addPositive(int $item): void { + if ($item <= 0) throw new InvalidArgumentException("Must be positive"); + $this->add($item); + } +} + +// Generic child +class TypedCollection extends Collection { + public function last(): T { + return $this->items[count($this->items) - 1]; + } +} + +// 1. Bound type child +$ic = new IntCollection(); +$ic->add(1); +$ic->add(2); +echo "1. first: " . $ic->first() . ", count: " . $ic->count() . "\n"; + +try { + $ic->add("bad"); +} catch (TypeError $e) { + echo "1. TypeError OK\n"; +} + +// 2. Grandchild +$pic = new PositiveIntCollection(); +$pic->addPositive(5); +echo "2. first: " . $pic->first() . "\n"; + +try { + $pic->addPositive(-1); +} catch (InvalidArgumentException $e) { + echo "2. validation OK\n"; +} + +// 3. Generic child with explicit type +$tc = new TypedCollection(); +$tc->add("hello"); +$tc->add("world"); +echo "3. first: " . $tc->first() . ", last: " . $tc->last() . "\n"; + +try { + $tc->add(42); +} catch (TypeError $e) { + echo "3. TypeError OK\n"; +} + +// 4. Generic child with explicit type +$floats = new TypedCollection(); +$floats->add(3.14); +$floats->add(2.72); +echo "4. first: " . $floats->first() . ", last: " . $floats->last() . "\n"; + +echo "Done.\n"; +?> +--EXPECT-- +1. first: 1, count: 2 +1. TypeError OK +2. first: 5 +2. validation OK +3. first: hello, last: world +3. TypeError OK +4. first: 3.14, last: 2.72 +Done. diff --git a/Zend/tests/generics/generic_param_name_collision.phpt b/Zend/tests/generics/generic_param_name_collision.phpt new file mode 100644 index 0000000000000..d24a3d0d28de7 --- /dev/null +++ b/Zend/tests/generics/generic_param_name_collision.phpt @@ -0,0 +1,61 @@ +--TEST-- +Generic class: generic param name that looks like a built-in type +--DESCRIPTION-- +Tests that generic type parameter names are resolved correctly +even when they have names that look like built-in types or classes. +--FILE-- + { + private V $data; + public function __construct(V $data) { $this->data = $data; } + public function get(): V { return $this->data; } +} + +$w = new Wrapper(42); +echo "1. " . $w->get() . "\n"; + +try { + $w2 = new Wrapper("bad"); +} catch (TypeError $e) { + echo "1. TypeError OK\n"; +} + +// Single-letter params +class Pair { + public function __construct(private A $a, private B $b) {} + public function first(): A { return $this->a; } + public function second(): B { return $this->b; } +} + +$p = new Pair("age", 25); +echo "2. " . $p->first() . ": " . $p->second() . "\n"; + +// Longer descriptive names +class Container { + private array $items = []; + public function add(ElementType $item): void { $this->items[] = $item; } + public function first(): ElementType { return $this->items[0]; } +} + +$c = new Container(); +$c->add("hello"); +echo "3. " . $c->first() . "\n"; + +try { + $c->add(42); +} catch (TypeError $e) { + echo "3. TypeError OK\n"; +} + +echo "Done.\n"; +?> +--EXPECT-- +1. 42 +1. TypeError OK +2. age: 25 +3. hello +3. TypeError OK +Done. diff --git a/Zend/tests/generics/generic_promoted_property.phpt b/Zend/tests/generics/generic_promoted_property.phpt new file mode 100644 index 0000000000000..f5d157cd2c2c4 --- /dev/null +++ b/Zend/tests/generics/generic_promoted_property.phpt @@ -0,0 +1,53 @@ +--TEST-- +Generic class: promoted properties with generic type parameters +--FILE-- + { + public function __construct(public T $value) {} +} + +$intBox = new Box(42); +echo $intBox->value . "\n"; // 42 + +// Type enforcement on promoted property +try { + $intBox->value = "hello"; +} catch (TypeError $e) { + echo "Property error: " . $e->getMessage() . "\n"; +} + +// Constructor type enforcement +try { + $bad = new Box("hello"); +} catch (TypeError $e) { + echo "Constructor error: " . $e->getMessage() . "\n"; +} + +// Multiple promoted properties +class Pair { + public function __construct( + public A $first, + public B $second, + ) {} +} + +$p = new Pair("hello", 42); +echo $p->first . " " . $p->second . "\n"; + +try { + $p->first = 123; +} catch (TypeError $e) { + echo "Pair error: " . $e->getMessage() . "\n"; +} + +echo "OK\n"; +?> +--EXPECTF-- +42 +Property error: Cannot assign string to property Box::$value of type int +Constructor error: Box::__construct(): Argument #1 ($value) must be of type int, string given, called in %s on line %d +hello 42 +Pair error: Cannot assign int to property Pair::$first of type string +OK diff --git a/Zend/tests/generics/generic_reflection.phpt b/Zend/tests/generics/generic_reflection.phpt new file mode 100644 index 0000000000000..9c921f6e18c28 --- /dev/null +++ b/Zend/tests/generics/generic_reflection.phpt @@ -0,0 +1,108 @@ +--TEST-- +Generic class: Reflection API for generic parameters and type arguments +--FILE-- + { + public T $value; + public function __construct(T $value) { $this->value = $value; } +} + +class Container { + public function get(K $key): V { throw new \Exception("not impl"); } +} + +function identity(T $value): T { return $value; } + +// Test ReflectionClass::isGeneric() +$rc = new ReflectionClass(Box::class); +var_dump($rc->isGeneric()); // true + +$rc2 = new ReflectionClass(stdClass::class); +var_dump($rc2->isGeneric()); // false + +// Test ReflectionClass::getGenericParameters() +$params = $rc->getGenericParameters(); +echo "Box params count: " . count($params) . "\n"; +echo "Box param 0 name: " . $params[0]->getName() . "\n"; +echo "Box param 0 constraint: "; +var_dump($params[0]->getConstraint()); +echo "Box param 0 invariant: "; +var_dump($params[0]->isInvariant()); + +// Test Container +$rc3 = new ReflectionClass(Container::class); +$params3 = $rc3->getGenericParameters(); +echo "Container params count: " . count($params3) . "\n"; + +echo "K name: " . $params3[0]->getName() . "\n"; +echo "K invariant: "; +var_dump($params3[0]->isInvariant()); +echo "K covariant: "; +var_dump($params3[0]->isCovariant()); + +echo "V name: " . $params3[1]->getName() . "\n"; +echo "V covariant: "; +var_dump($params3[1]->isCovariant()); +echo "V contravariant: "; +var_dump($params3[1]->isContravariant()); +echo "V constraint: " . $params3[1]->getConstraint()->getName() . "\n"; + +// Test __toString +echo "K __toString: " . $params3[0] . "\n"; +echo "V __toString: " . $params3[1] . "\n"; + +// Test ReflectionObject::getGenericArguments() +$box = new Box(42); +$ro = new ReflectionObject($box); +var_dump($ro->isGeneric()); // true +$args = $ro->getGenericArguments(); +echo "Box args count: " . count($args) . "\n"; +echo "Box arg 0: " . $args[0]->getName() . "\n"; + +// Test non-generic object +$obj = new stdClass(); +$ro2 = new ReflectionObject($obj); +$args2 = $ro2->getGenericArguments(); +echo "stdClass args count: " . count($args2) . "\n"; + +// Test generic function reflection +$rf = new ReflectionFunction('identity'); +var_dump($rf->isGeneric()); // true +$fparams = $rf->getGenericParameters(); +echo "identity params count: " . count($fparams) . "\n"; +echo "identity param 0 name: " . $fparams[0]->getName() . "\n"; + +// Test non-generic function +$rf2 = new ReflectionFunction('strlen'); +var_dump($rf2->isGeneric()); // false + +echo "OK\n"; +?> +--EXPECT-- +bool(true) +bool(false) +Box params count: 1 +Box param 0 name: T +Box param 0 constraint: NULL +Box param 0 invariant: bool(true) +Container params count: 2 +K name: K +K invariant: bool(true) +K covariant: bool(false) +V name: V +V covariant: bool(true) +V contravariant: bool(false) +V constraint: Countable +K __toString: K +V __toString: out V: Countable +bool(true) +Box args count: 1 +Box arg 0: int +stdClass args count: 0 +bool(true) +identity params count: 1 +identity param 0 name: T +bool(false) +OK diff --git a/Zend/tests/generics/generic_reject_never_extends.phpt b/Zend/tests/generics/generic_reject_never_extends.phpt new file mode 100644 index 0000000000000..5172d23c39a7c --- /dev/null +++ b/Zend/tests/generics/generic_reject_never_extends.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic class: reject never as generic type argument in extends +--FILE-- + { + public function __construct(public T $value) {} +} +class NeverBox extends Box {} +?> +--EXPECTF-- +Fatal error: never cannot be used as a generic type argument in %s on line %d diff --git a/Zend/tests/generics/generic_reject_never_new.phpt b/Zend/tests/generics/generic_reject_never_new.phpt new file mode 100644 index 0000000000000..38e2065586cec --- /dev/null +++ b/Zend/tests/generics/generic_reject_never_new.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic class: reject never as generic type argument in new +--FILE-- + { + public function __construct(public T $value) {} +} +$b = new Box(42); +?> +--EXPECTF-- +Fatal error: never cannot be used as a generic type argument in %s on line %d diff --git a/Zend/tests/generics/generic_reject_void_extends.phpt b/Zend/tests/generics/generic_reject_void_extends.phpt new file mode 100644 index 0000000000000..39c030bacae2b --- /dev/null +++ b/Zend/tests/generics/generic_reject_void_extends.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic class: reject void as generic type argument in extends +--FILE-- + { + public function __construct(public T $value) {} +} +class VoidBox extends Box {} +?> +--EXPECTF-- +Fatal error: void cannot be used as a generic type argument in %s on line %d diff --git a/Zend/tests/generics/generic_reject_void_new.phpt b/Zend/tests/generics/generic_reject_void_new.phpt new file mode 100644 index 0000000000000..0e3eda4df1ee8 --- /dev/null +++ b/Zend/tests/generics/generic_reject_void_new.phpt @@ -0,0 +1,11 @@ +--TEST-- +Generic class: reject void as generic type argument in new +--FILE-- + { + public function __construct(public T $value) {} +} +$b = new Box(42); +?> +--EXPECTF-- +Fatal error: void cannot be used as a generic type argument in %s on line %d diff --git a/Zend/tests/generics/generic_resolved_masks_complex.phpt b/Zend/tests/generics/generic_resolved_masks_complex.phpt new file mode 100644 index 0000000000000..5de9487012c7c --- /dev/null +++ b/Zend/tests/generics/generic_resolved_masks_complex.phpt @@ -0,0 +1,90 @@ +--TEST-- +Generic class: resolved_masks slow path for complex types (generic class refs, nested) +--DESCRIPTION-- +Tests that the resolved_masks optimization correctly falls through to the +slow path for complex types like generic class references (Box), +class types, and nested generics. The mask is 0 for these, forcing +the full type check. +--FILE-- + { + public T $value; + public function __construct(T $value) { $this->value = $value; } + public function get(): T { return $this->value; } +} + +// Generic class ref as type arg: Box> +// resolved_masks[0] should be 0 (complex type), falling through to slow path +$inner = new Box(42); +$outer = new Box>($inner); +echo "nested create: OK\n"; + +// Method return with nested generic +echo $outer->get()->get() . "\n"; + +// Property access on nested generic +echo $outer->value->value . "\n"; + +// Constructor type check (slow path: verifies Box arg matches T=Box) +try { + $bad = new Box>("not a box"); +} catch (TypeError $e) { + echo "nested ctor reject: OK\n"; +} + +// Wrong inner type +$wrongInner = new Box("hello"); +try { + $bad2 = new Box>($wrongInner); +} catch (TypeError $e) { + echo "wrong inner type reject: OK\n"; +} + +// Property assignment on nested generic (triggers slow path type check) +try { + $outer->get()->value = "not an int"; +} catch (TypeError $e) { + echo "nested prop reject: OK\n"; +} + +// Valid property assignment on inner +$outer->get()->value = 100; +echo $outer->get()->value . "\n"; + +// Class type arg (non-generic class) +interface Printable { + public function display(): string; +} + +class Label implements Printable { + public function __construct(private string $text) {} + public function display(): string { return $this->text; } +} + +class Wrapper { + public function __construct(private T $item) {} + public function show(): string { return $this->item->display(); } +} + +$w = new Wrapper