diff --git a/Zend/tests/type_declarations/abstract_generics/abstract_generic_001.phpt b/Zend/tests/type_declarations/abstract_generics/abstract_generic_001.phpt
new file mode 100644
index 0000000000000..251724b6dd840
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/abstract_generic_001.phpt
@@ -0,0 +1,31 @@
+--TEST--
+Abstract generic types basic
+--FILE--
+<?php
+
+interface I<T> {
+    public function foo(T $param): T;
+}
+
+class CS implements I<string> {
+    public function foo(string $param): string {
+        return $param . '!';
+    }
+}
+
+class CI implements I<int> {
+    public function foo(int $param): int {
+        return $param + 42;
+    }
+}
+
+$cs = new CS();
+var_dump($cs->foo("Hello"));
+
+$ci = new CI();
+var_dump($ci->foo(5));
+
+?>
+--EXPECT--
+string(6) "Hello!"
+int(47)
diff --git a/Zend/tests/type_declarations/abstract_generics/big_example.phpt b/Zend/tests/type_declarations/abstract_generics/big_example.phpt
new file mode 100644
index 0000000000000..0beea672751eb
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/big_example.phpt
@@ -0,0 +1,100 @@
+--TEST--
+Concrete example of using AT
+--CREDITS--
+Levi Morrison
+--FILE--
+<?php declare(strict_types=1);
+
+namespace Sequence;
+
+// No null. This is probably going to be painful, but let's try it.
+interface Sequence<Item : object|array|string|float|int|bool>
+{
+    function next(): ?Item;
+
+    /**
+     * @param callable(Item, Item): Item $f
+     * @return ?Item
+     */
+    function reduce(callable $f): ?Item;
+}
+
+final class StringTablePair
+{
+    public function __construct(
+        public readonly string $string,
+        public readonly int $id,
+    ) {}
+}
+
+final class StringTableSequence implements Sequence<StringTablePair>
+{
+    private array $strings;
+
+    public function __construct(StringTable $string_table) {
+        $this->strings = $string_table->to_assoc_array();
+    }
+
+    function next(): ?StringTablePair
+    {
+        $key = \array_key_first($this->strings);
+        if (!isset($key)) {
+            return null;
+        }
+        $value = \array_shift($this->strings);
+        return new StringTablePair($key, $value);
+    }
+
+    /**
+     * @param callable(Item, Item): Item $f
+     * @return ?Item
+     */
+    function reduce(callable $f): ?StringTablePair
+    {
+        $reduction = $this->next();
+        if (!isset($reduction)) {
+            return null;
+        }
+
+        while (($next = $this->next()) !== null) {
+            $reduction = $f($reduction, $next);
+        }
+        return $reduction;
+    }
+}
+
+final class StringTable
+{
+    private array $strings = ["" => 0];
+
+    public function __construct() {}
+
+    public function offsetGet(string $offset): int
+    {
+        return $this->strings[$offset] ?? throw new \Exception();
+    }
+
+    public function offsetExists(string $offset): bool
+    {
+        return \isset($this->strings[$offset]);
+    }
+
+    public function intern(string $str): int
+    {
+        return $this->strings[$str]
+            ?? ($this->strings[$str] = \count($this->strings));
+    }
+
+    public function to_sequence(): StringTableSequence
+    {
+        return new StringTableSequence($this);
+    }
+
+    public function to_assoc_array(): array {
+        return $this->strings;
+    }
+}
+
+?>
+--EXPECTF--
+Fatal error: Generic type cannot be part of a union type in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_different_bound_types.phpt b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_different_bound_types.phpt
new file mode 100644
index 0000000000000..c8dd8d7eda0f0
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_different_bound_types.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Implicit interface inheritance with different bound types
+--FILE--
+<?php
+
+interface I1<T> {
+    public function foo(T $param): T;
+}
+
+interface I2<T> extends I1<T> {
+    public function bar(int $o, T $param): T;
+}
+
+class C implements I2<float>, I1<string> {
+    public function foo(float $param): float {}
+    public function bar(int $o, float $param): float {}
+}
+
+?>
+--EXPECTF--
+Fatal error: Bound type T for interface I1 implemented explicitly in C with type string must match the implicitly bound type float from interface I2 in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_different_bound_types2.phpt b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_different_bound_types2.phpt
new file mode 100644
index 0000000000000..eda45f32ef6d0
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_different_bound_types2.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Implicit interface inheritance with different bound types 2
+--FILE--
+<?php
+
+interface I1<T> {
+    public function foo(T $param): T;
+}
+
+interface I2<T> extends I1<T> {
+    public function bar(int $o, T $param): T;
+}
+
+class C implements I1<string>, I2<float> {
+    public function foo(string $param): string {}
+    public function bar(int $o, float $param): float {}
+}
+
+?>
+--EXPECTF--
+Fatal error: Bound type T for interface I1 implemented explicitly in C with type string must match the implicitly bound type float from interface I2 in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_different_bound_types3.phpt b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_different_bound_types3.phpt
new file mode 100644
index 0000000000000..6a37e34bbc184
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_different_bound_types3.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Implicit interface inheritance with different bound types 3
+--FILE--
+<?php
+
+interface I1<T> {
+    public function foo(T $param): T;
+}
+
+interface I2 extends I1<float> {
+    public function bar(int $o, float $param): float;
+}
+
+class C implements I2, I1<string> {
+    public function foo(float $param): float {}
+    public function bar(int $o, float $param): float {}
+}
+
+?>
+--EXPECTF--
+Fatal error: Bound type T for interface I1 implemented explicitly in C with type string must match the implicitly bound type float from interface I2 in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_different_bound_types4.phpt b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_different_bound_types4.phpt
new file mode 100644
index 0000000000000..49029e3904d00
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_different_bound_types4.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Implicit interface inheritance with different bound types 4
+--FILE--
+<?php
+
+interface I1<T> {
+    public function foo(T $param): T;
+}
+
+interface I2 extends I1<float> {
+    public function bar(int $o, float $param): float;
+}
+
+class C implements I1<string>, I2 {
+    public function foo(string $param): string {}
+    public function bar(int $o, float $param): float {}
+}
+
+?>
+--EXPECTF--
+Fatal error: Bound type T for interface I1 implemented explicitly in C with type string must match the implicitly bound type float from interface I2 in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_no_bound_types.phpt b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_no_bound_types.phpt
new file mode 100644
index 0000000000000..3595c2b650298
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_no_bound_types.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Implicit interface inheritance missing bound types
+--FILE--
+<?php
+
+interface I1<T> {
+    public function foo(T $param): T;
+}
+
+interface I2<T> extends I1<T> {
+    public function bar(int $o, T $param): T;
+}
+
+class C implements I2<float>, I1 {
+    public function foo(float $param): float {}
+    public function bar(int $o, float $param): float {}
+}
+
+?>
+--EXPECTF--
+Fatal error: Interface I1 expects 1 generic parameters, 0 given in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_no_bound_types2.phpt b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_no_bound_types2.phpt
new file mode 100644
index 0000000000000..4f505ebb0a9e4
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_no_bound_types2.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Implicit interface inheritance missing bound types 2
+--FILE--
+<?php
+
+interface I1<T> {
+    public function foo(T $param): T;
+}
+
+interface I2<T> extends I1<T> {
+    public function bar(int $o, T $param): T;
+}
+
+class C implements I1, I2<float> {
+    public function foo(float $param): float {}
+    public function bar(int $o, float $param): float {}
+}
+
+?>
+--EXPECTF--
+Fatal error: Interface I1 expects 1 generic parameters, 0 given in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_no_bound_types3.phpt b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_no_bound_types3.phpt
new file mode 100644
index 0000000000000..fc8fe27cf2420
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_no_bound_types3.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Implicit interface inheritance missing bound types 3
+--FILE--
+<?php
+
+interface I1<T> {
+    public function foo(T $param): T;
+}
+
+interface I2 extends I1<string> {
+    public function bar(int $o, string $param): string;
+}
+
+class C implements I1, I2 {
+    public function foo(string $param): string {}
+    public function bar(int $o, string $param): string {}
+}
+
+?>
+--EXPECTF--
+Fatal error: Interface I1 expects 1 generic parameters, 0 given in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_no_bound_types4.phpt b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_no_bound_types4.phpt
new file mode 100644
index 0000000000000..22ae5a730a95b
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_no_bound_types4.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Implicit interface inheritance missing bound types 4
+--FILE--
+<?php
+
+interface I1<T> {
+    public function foo(T $param): T;
+}
+
+interface I2 extends I1<string> {
+    public function bar(int $o, string $param): string;
+}
+
+class C implements I2, I1 {
+    public function foo(string $param): string {}
+    public function bar(int $o, string $param): string {}
+}
+
+?>
+--EXPECTF--
+Fatal error: Interface I1 expects 1 generic parameters, 0 given in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_same_bound_types.phpt b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_same_bound_types.phpt
new file mode 100644
index 0000000000000..4e3e09e9a872b
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_same_bound_types.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Implicit interface inheritance with same bound types
+--FILE--
+<?php
+
+interface I1<T> {
+    public function foo(T $param): T;
+}
+
+interface I2<T> extends I1<T> {
+    public function bar(int $o, T $param): T;
+}
+
+class C implements I2<string>, I1<string> {
+    public function foo(string $param): string {}
+    public function bar(int $o, string $param): string {}
+}
+
+?>
+DONE
+--EXPECT--
+DONE
diff --git a/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_same_bound_types2.phpt b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_same_bound_types2.phpt
new file mode 100644
index 0000000000000..d6ca51381c9df
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/bound_types/implicit_interface_same_bound_types2.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Implicit interface inheritance with same bound types 2
+--FILE--
+<?php
+
+interface I1<T> {
+    public function foo(T $param): T;
+}
+
+interface I2<T> extends I1<T> {
+    public function bar(int $o, T $param): T;
+}
+
+class C implements I1<string>, I2<string> {
+    public function foo(string $param): string {}
+    public function bar(int $o, string $param): string {}
+}
+
+?>
+DONE
+--EXPECT--
+DONE
diff --git a/Zend/tests/type_declarations/abstract_generics/constraints/abstract_generic_type_with_constraint.phpt b/Zend/tests/type_declarations/abstract_generics/constraints/abstract_generic_type_with_constraint.phpt
new file mode 100644
index 0000000000000..2f1c57492c833
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/constraints/abstract_generic_type_with_constraint.phpt
@@ -0,0 +1,31 @@
+--TEST--
+Abstract generic type with a constraint
+--FILE--
+<?php
+
+interface I<T : int|string> {
+    public function foo(T $param): T;
+}
+
+class CS implements I<string> {
+    public function foo(string $param): string {
+        return $param . '!';
+    }
+}
+
+class CI implements I<int> {
+    public function foo(int $param): int {
+        return $param + 42;
+    }
+}
+
+$cs = new CS();
+var_dump($cs->foo("Hello"));
+
+$ci = new CI();
+var_dump($ci->foo(5));
+
+?>
+--EXPECT--
+string(6) "Hello!"
+int(47)
diff --git a/Zend/tests/type_declarations/abstract_generics/constraints/abstract_generic_type_with_constraint_failed.phpt b/Zend/tests/type_declarations/abstract_generics/constraints/abstract_generic_type_with_constraint_failed.phpt
new file mode 100644
index 0000000000000..440d1868e6162
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/constraints/abstract_generic_type_with_constraint_failed.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Abstract generic type with a constraint that is not satisfied
+--FILE--
+<?php
+
+interface I<T : int|string> {
+    public function foo(T $param): T;
+}
+
+class C implements I<float> {
+    public function foo(float $param): float {}
+}
+
+?>
+--EXPECTF--
+Fatal error: Bound type float is not a subtype of the constraint type string|int of generic type T of interface I in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/constraints/errors/using_generic_type_as_constraint.phpt b/Zend/tests/type_declarations/abstract_generics/constraints/errors/using_generic_type_as_constraint.phpt
new file mode 100644
index 0000000000000..7a964dd0f60a5
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/constraints/errors/using_generic_type_as_constraint.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Cannot use generic type as a constraint
+--FILE--
+<?php
+
+interface I<T1, T2 : T1> {
+    public function foo(T $param): T;
+}
+
+?>
+--EXPECTF--
+Fatal error: Cannot use generic parameter T1 to constrain generic parameter T2 in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/constraints/errors/using_never_as_constraint.phpt b/Zend/tests/type_declarations/abstract_generics/constraints/errors/using_never_as_constraint.phpt
new file mode 100644
index 0000000000000..485841f50ee59
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/constraints/errors/using_never_as_constraint.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Cannot use never as a constraint
+--FILE--
+<?php
+
+interface I<T : never> {
+    public function foo(T $param): T;
+}
+
+?>
+--EXPECTF--
+Fatal error: Cannot use static, void, or never to constrain generic parameter T in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/constraints/errors/using_static_as_constraint.phpt b/Zend/tests/type_declarations/abstract_generics/constraints/errors/using_static_as_constraint.phpt
new file mode 100644
index 0000000000000..86bcc1d2b7613
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/constraints/errors/using_static_as_constraint.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Cannot use void as a constraint
+--FILE--
+<?php
+
+interface I<T : static> {
+    public function foo(T $param): T;
+}
+
+?>
+--EXPECTF--
+Fatal error: Cannot use static, void, or never to constrain generic parameter T in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/constraints/errors/using_void_as_constraint.phpt b/Zend/tests/type_declarations/abstract_generics/constraints/errors/using_void_as_constraint.phpt
new file mode 100644
index 0000000000000..1f727f33665ae
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/constraints/errors/using_void_as_constraint.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Cannot use void as a constraint
+--FILE--
+<?php
+
+interface I<T : void> {
+    public function foo(T $param): T;
+}
+
+?>
+--EXPECTF--
+Fatal error: Cannot use static, void, or never to constrain generic parameter T in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/constraints/extended_interface_abstract_generic_constraint_failure.phpt b/Zend/tests/type_declarations/abstract_generics/constraints/extended_interface_abstract_generic_constraint_failure.phpt
new file mode 100644
index 0000000000000..4203cc15b3ec9
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/constraints/extended_interface_abstract_generic_constraint_failure.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Abstract generic type behaviour in extended interface violates type constraint
+--FILE--
+<?php
+
+interface I<T : int|string|(Traversable&Countable)> {
+    public function foo(T $param): T;
+}
+
+interface I2<T> extends I<T> {
+    public function bar(int $o, T $param): T;
+}
+
+class C implements I2<string> {
+    public function foo(string $param): string {}
+    public function bar(int $o, float $param): float {}
+}
+
+?>
+--EXPECTF--
+Fatal error: Constraint type mixed of generic type T of interface I2 is not a subtype of the constraint type (Traversable&Countable)|string|int of generic type T of interface I in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_intersection1.phpt b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_intersection1.phpt
new file mode 100644
index 0000000000000..a4f2defb5d0d5
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_intersection1.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Abstract generic type cannot be in intersection (simple intersection with class type)
+--FILE--
+<?php
+
+interface I<T> {
+    public function foo(T&Traversable $param): T&Traversable;
+}
+
+?>
+--EXPECTF--
+Fatal error: Generic type cannot be part of an intersection type in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_intersection2.phpt b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_intersection2.phpt
new file mode 100644
index 0000000000000..bbffdc731ae4f
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_intersection2.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Abstract generic type cannot be in intersection (DNF type)
+--FILE--
+<?php
+
+interface I<T> {
+    public function foo(stdClass|(T&Traversable) $param): stdClass|(T&Traversable);
+}
+
+?>
+--EXPECTF--
+Fatal error: Generic type cannot be part of an intersection type in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_union1.phpt b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_union1.phpt
new file mode 100644
index 0000000000000..2880ff7007ff3
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_union1.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Abstract generic type cannot be in union (simple union with built-in type)
+--FILE--
+<?php
+
+interface I<T> {
+    public function foo(T|int $param): T|int;
+}
+
+?>
+--EXPECTF--
+Fatal error: Generic type cannot be part of a union type in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_union2.phpt b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_union2.phpt
new file mode 100644
index 0000000000000..a1b9d7e2f0fd4
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_union2.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Abstract generic type cannot be in union (simple union with class type)
+--FILE--
+<?php
+
+interface I<T> {
+    public function foo(T|stdClass $param): T|stdClass;
+}
+
+?>
+--EXPECTF--
+Fatal error: Generic type cannot be part of a union type in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_union3.phpt b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_union3.phpt
new file mode 100644
index 0000000000000..3f93897a21655
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_union3.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Abstract generic type cannot be in union (DNF type)
+--FILE--
+<?php
+
+interface I<T> {
+    public function foo(T|(Traversable&Countable) $param): T|(Traversable&Countable);
+}
+
+?>
+--EXPECTF--
+Fatal error: Generic type cannot be part of a union type in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_union4.phpt b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_union4.phpt
new file mode 100644
index 0000000000000..d69a58e155643
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_union4.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Abstract generic type cannot be in union (nullable type union with ?)
+--FILE--
+<?php
+
+interface I<T> {
+    public function foo(?T $param): ?T;
+}
+
+?>
+--EXPECTF--
+Fatal error: Generic type cannot be part of a union type in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_union5.phpt b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_union5.phpt
new file mode 100644
index 0000000000000..26287188e31cc
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_cannot_be_in_union5.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Abstract generic type cannot be in union (forced allowed null)
+--FILE--
+<?php
+
+interface I<T> {
+    public function foo(T $param = null): T;
+}
+
+?>
+--EXPECTF--
+Fatal error: Generic type cannot be part of a union type (implicitly nullable due to default null value) in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_in_class.phpt b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_in_class.phpt
new file mode 100644
index 0000000000000..a037af45bf87d
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_in_class.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Abstract generic type in class is invalid
+--FILE--
+<?php
+
+class C<T> {
+    public function foo(T $param): T;
+}
+
+?>
+--EXPECTF--
+Parse error: syntax error, unexpected token "<", expecting "{" in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_in_trait.phpt b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_in_trait.phpt
new file mode 100644
index 0000000000000..b00a523069a6b
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_in_trait.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Abstract generic type in trait is invalid
+--FILE--
+<?php
+
+trait C<T> {
+    public function foo(T $param): T;
+}
+
+?>
+--EXPECTF--
+Parse error: syntax error, unexpected token "<", expecting "{" in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_redeclared.phpt b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_redeclared.phpt
new file mode 100644
index 0000000000000..148baa42f3b02
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/errors/abstract_generic_redeclared.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Abstract generic type that is redeclared
+--FILE--
+<?php
+
+interface I<T, S, T> {
+    public function foo(T&Traversable $param): T&Traversable;
+}
+
+?>
+--EXPECTF--
+Fatal error: Duplicate generic parameter T in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/errors/no_bound_abstract_generic_type.phpt b/Zend/tests/type_declarations/abstract_generics/errors/no_bound_abstract_generic_type.phpt
new file mode 100644
index 0000000000000..932ff702388e2
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/errors/no_bound_abstract_generic_type.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Implementing class does not bind any abstract generic type
+--FILE--
+<?php
+
+interface I<T> {
+    public function foo(T $param): T;
+}
+
+class C implements I {
+    public function foo(float $param): float {}
+}
+
+?>
+--EXPECTF--
+Fatal error: Interface I expects 1 generic parameters, 0 given in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/errors/no_bound_abstract_generic_type_with_constraint.phpt b/Zend/tests/type_declarations/abstract_generics/errors/no_bound_abstract_generic_type_with_constraint.phpt
new file mode 100644
index 0000000000000..bce328f805b1d
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/errors/no_bound_abstract_generic_type_with_constraint.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Implementing class does not bind any abstract generic type
+--FILE--
+<?php
+
+interface I<T : int|string> {
+    public function foo(T $param): T;
+}
+
+class C implements I {
+    public function foo(float $param): float {}
+}
+
+?>
+--EXPECTF--
+Fatal error: Interface I expects 1 generic parameters, 0 given in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/errors/no_bound_abstract_generic_type_with_prior_bound_types.phpt b/Zend/tests/type_declarations/abstract_generics/errors/no_bound_abstract_generic_type_with_prior_bound_types.phpt
new file mode 100644
index 0000000000000..417c90a8d2aae
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/errors/no_bound_abstract_generic_type_with_prior_bound_types.phpt
@@ -0,0 +1,20 @@
+--TEST--
+Implementing class does not bind any abstract generic type
+--FILE--
+<?php
+
+interface I1<T> {
+    public function foo(T $param): T;
+}
+interface I2<T> {
+    public function bar(T $param): T;
+}
+
+class C implements I1<float>, I2 {
+    public function foo(float $param): float {}
+    public function bar(string $param): string {}
+}
+
+?>
+--EXPECTF--
+Fatal error: Interface I2 expects 1 generic parameters, 0 given in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/extended_interface_abstract_generic_types_1_to_2.phpt b/Zend/tests/type_declarations/abstract_generics/extended_interface_abstract_generic_types_1_to_2.phpt
new file mode 100644
index 0000000000000..3b5ebbde839ef
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/extended_interface_abstract_generic_types_1_to_2.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Abstract generic type behaviour in extended interface 2 generic type to 1
+--FILE--
+<?php
+
+interface I<T1, T2> {
+    public function foo(T1 $param): T2;
+}
+
+interface I2<S> extends I<S, S> {
+    public function bar(int $o, S $param): S;
+}
+
+class C implements I2<string> {
+    public function foo(string $param): string {}
+    public function bar(int $o, string $param): string {}
+}
+
+?>
+DONE
+--EXPECT--
+DONE
diff --git a/Zend/tests/type_declarations/abstract_generics/extended_interface_abstract_generic_types_basic.phpt b/Zend/tests/type_declarations/abstract_generics/extended_interface_abstract_generic_types_basic.phpt
new file mode 100644
index 0000000000000..038e66ef63e3c
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/extended_interface_abstract_generic_types_basic.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Abstract generic type behaviour in extended interface
+--FILE--
+<?php
+
+interface I<T1> {
+    public function foo(T1 $param): T1;
+}
+
+interface I2<T2> extends I<T2> {
+    public function bar(int $o, T2 $param): T2;
+}
+
+class C implements I2<string> {
+    public function foo(string $param): string {}
+    public function bar(int $o, string $param): string {}
+}
+
+?>
+DONE
+--EXPECT--
+DONE
diff --git a/Zend/tests/type_declarations/abstract_generics/extended_interface_new_abstract_generic_type.phpt b/Zend/tests/type_declarations/abstract_generics/extended_interface_new_abstract_generic_type.phpt
new file mode 100644
index 0000000000000..8fa85be75ca2c
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/extended_interface_new_abstract_generic_type.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Abstract generic type behaviour in extended interface
+--FILE--
+<?php
+
+interface I<T : int|string|(Traversable&Countable)> {
+    public function foo(T $param): T;
+}
+
+interface I2<T : int|string|(Traversable&Countable), T2 : float|bool|stdClass> extends I<T> {
+    public function bar(T2 $o, T $param): T2;
+}
+
+class C implements I2<string, float> {
+    public function foo(string $param): string {}
+    public function bar(float $o, string $param): float {}
+}
+
+?>
+DONE
+--EXPECT--
+DONE
diff --git a/Zend/tests/type_declarations/abstract_generics/extended_interface_new_abstract_generic_type2.phpt b/Zend/tests/type_declarations/abstract_generics/extended_interface_new_abstract_generic_type2.phpt
new file mode 100644
index 0000000000000..784fd9a0aa33b
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/extended_interface_new_abstract_generic_type2.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Abstract generic type behaviour in extended interface
+--FILE--
+<?php
+
+interface I<T : int|string|(Traversable&Countable)> {
+    public function foo(T $param): T;
+}
+
+interface I2<T : float|bool|stdClass, T2 : int|string|(Traversable&Countable)> extends I<T2> {
+    public function bar(T $o, T2 $param): T;
+}
+
+class C implements I2<float, string> {
+    public function foo(string $param): string {}
+    public function bar(float $o, string $param): float {}
+}
+
+?>
+DONE
+--EXPECT--
+DONE
diff --git a/Zend/tests/type_declarations/abstract_generics/extended_interface_redeclares_method.phpt b/Zend/tests/type_declarations/abstract_generics/extended_interface_redeclares_method.phpt
new file mode 100644
index 0000000000000..359d94d594374
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/extended_interface_redeclares_method.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Abstract generic type behaviour in extended interface which redeclares method
+--FILE--
+<?php
+
+interface I<T> {
+    public function foo(T $param): int;
+}
+
+interface J extends I<float> {
+    public function foo(float $param): int;
+}
+
+?>
+DONE
+--EXPECT--
+DONE
+
diff --git a/Zend/tests/type_declarations/abstract_generics/extended_interface_with_generics_redeclares_method_contravariant.phpt b/Zend/tests/type_declarations/abstract_generics/extended_interface_with_generics_redeclares_method_contravariant.phpt
new file mode 100644
index 0000000000000..44ca267727701
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/extended_interface_with_generics_redeclares_method_contravariant.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Abstract generic type behaviour in extended interface which redeclares method generic type contravariant
+--FILE--
+<?php
+
+interface I<T> {
+    public function foo(T $param): int;
+}
+
+interface J<S> extends I<S> {
+    public function foo(S $param): int;
+}
+
+?>
+DONE
+--EXPECT--
+DONE
diff --git a/Zend/tests/type_declarations/abstract_generics/extended_interface_with_generics_redeclares_method_covariant.phpt b/Zend/tests/type_declarations/abstract_generics/extended_interface_with_generics_redeclares_method_covariant.phpt
new file mode 100644
index 0000000000000..b73f71c68a14b
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/extended_interface_with_generics_redeclares_method_covariant.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Abstract generic type behaviour in extended interface which redeclares method generic type covariant
+--FILE--
+<?php
+
+interface I<T> {
+    public function foo(int $param): T;
+}
+
+interface J<S> extends I<S> {
+    public function foo(int $param): S;
+}
+
+?>
+DONE
+--EXPECT--
+DONE
+
diff --git a/Zend/tests/type_declarations/abstract_generics/extended_twice_interface_abstract_generic_types_basic.phpt b/Zend/tests/type_declarations/abstract_generics/extended_twice_interface_abstract_generic_types_basic.phpt
new file mode 100644
index 0000000000000..14ce05d3e248b
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/extended_twice_interface_abstract_generic_types_basic.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Abstract generic type behaviour in extended interface
+--FILE--
+<?php
+
+interface I1<T1> {
+    public function foo(T1 $param): T1;
+}
+
+interface I2<T2> extends I1<T2> {
+    public function bar(int $o, T2 $param): T2;
+}
+
+interface I3<T3> extends I2<T3> {
+    public function foobar(T3 $a, float $b): float;
+}
+
+class C implements I3<string> {
+    public function foo(string $param): string {}
+    public function bar(int $o, string $param): string {}
+    public function foobar(string $a, float $b): float {}
+}
+
+?>
+DONE
+--EXPECT--
+DONE
diff --git a/Zend/tests/type_declarations/abstract_generics/extended_twice_interface_generic_into_concrete.phpt b/Zend/tests/type_declarations/abstract_generics/extended_twice_interface_generic_into_concrete.phpt
new file mode 100644
index 0000000000000..f42fb17c691b0
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/extended_twice_interface_generic_into_concrete.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Abstract generic type behaviour in extended interface
+--FILE--
+<?php
+
+interface I1<T1> {
+    public function foo(T1 $param): T1;
+}
+
+interface I2<T2> extends I1<T2> {
+    public function bar(int $o, T2 $param): T2;
+}
+
+interface I3 extends I2<string> {
+    public function foobar(string $a, float $b): float;
+}
+
+class C implements I3 {
+    public function foo(string $param): string {}
+    public function bar(int $o, string $param): string {}
+    public function foobar(string $a, float $b): float {}
+}
+
+?>
+DONE
+--EXPECT--
+DONE
diff --git a/Zend/tests/type_declarations/abstract_generics/multiple_abstract_generic_type.phpt b/Zend/tests/type_declarations/abstract_generics/multiple_abstract_generic_type.phpt
new file mode 100644
index 0000000000000..63b3bf6f202f7
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/multiple_abstract_generic_type.phpt
@@ -0,0 +1,72 @@
+--TEST--
+Multiple abstract generic type
+--FILE--
+<?php
+
+interface I<K, V> {
+    public function set(K $key, V $value): void;
+    public function get(K $key): V;
+}
+
+class C1 implements I<int, string> {
+    public array $a = [];
+    public function set(int $key, string $value): void {
+        $this->a[$key] = $value . '!';
+    }
+    public function get(int $key): string {
+        return $this->a[$key];
+    }
+}
+
+class C2 implements I<string, object> {
+    public array $a = [];
+    public function set(string $key, object $value): void {
+        $this->a[$key] = $value;
+    }
+    public function get(string $key): object {
+        return $this->a[$key];
+    }
+}
+
+$c1 = new C1();
+$c1->set(5, "Hello");
+var_dump($c1->a);
+var_dump($c1->get(5));
+
+$c2 = new C2();
+$c2->set('C1', $c1);
+var_dump($c2->a);
+var_dump($c2->get('C1'));
+
+try {
+    $c1->set('blah', "Hello");
+} catch (\Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+
+?>
+--EXPECTF--
+array(1) {
+  [5]=>
+  string(6) "Hello!"
+}
+string(6) "Hello!"
+array(1) {
+  ["C1"]=>
+  object(C1)#1 (1) {
+    ["a"]=>
+    array(1) {
+      [5]=>
+      string(6) "Hello!"
+    }
+  }
+}
+object(C1)#1 (1) {
+  ["a"]=>
+  array(1) {
+    [5]=>
+    string(6) "Hello!"
+  }
+}
+TypeError: C1::set(): Argument #1 ($key) must be of type int, string given, called in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/variance/extended_interface_concrete_to_generic_contravariance.phpt b/Zend/tests/type_declarations/abstract_generics/variance/extended_interface_concrete_to_generic_contravariance.phpt
new file mode 100644
index 0000000000000..602b917ea5fc0
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/variance/extended_interface_concrete_to_generic_contravariance.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Redeclaring a method that has a concrete type into a generic type (contravariance)
+--FILE--
+<?php
+
+interface I {
+    public function foo(int $param): int;
+}
+
+interface J<S> extends I {
+    public function foo(S $param): int;
+}
+
+?>
+DONE
+--EXPECTF--
+Fatal error: Declaration of J<S : mixed>::foo(<S> $param): int must be compatible with I::foo(int $param): int %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/variance/extended_interface_concrete_to_generic_covariance.phpt b/Zend/tests/type_declarations/abstract_generics/variance/extended_interface_concrete_to_generic_covariance.phpt
new file mode 100644
index 0000000000000..005a055e3f0fd
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/variance/extended_interface_concrete_to_generic_covariance.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Redeclaring a method that has a concrete type into a generic type (covariance)
+--FILE--
+<?php
+
+interface I {
+    public function foo(int $param): int;
+}
+
+interface J<S> extends I {
+    public function foo(int $param): S;
+}
+
+?>
+DONE
+--EXPECTF--
+Fatal error: Declaration of J<S : mixed>::foo(int $param): <S> must be compatible with I::foo(int $param): int %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/variance/extended_interface_with_generics_redeclares_method_invalid_contravariance.phpt b/Zend/tests/type_declarations/abstract_generics/variance/extended_interface_with_generics_redeclares_method_invalid_contravariance.phpt
new file mode 100644
index 0000000000000..f5360557bb6a3
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/variance/extended_interface_with_generics_redeclares_method_invalid_contravariance.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Abstract generic type behaviour in extended interface which redeclares method but does not use bound generic type (contravariance)
+--FILE--
+<?php
+
+interface I<T> {
+    public function foo(T $param): int;
+}
+
+interface J<S> extends I<S> {
+    public function foo(int $param): int;
+}
+
+?>
+DONE
+--EXPECTF--
+Fatal error: Declaration of J<S : mixed>::foo(int $param): int must be compatible with I<T : <S>>::foo(<T : <S>> $param): int %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/variance/extended_interface_with_generics_redeclares_method_invalid_covariance.phpt b/Zend/tests/type_declarations/abstract_generics/variance/extended_interface_with_generics_redeclares_method_invalid_covariance.phpt
new file mode 100644
index 0000000000000..beb571606cf0f
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/variance/extended_interface_with_generics_redeclares_method_invalid_covariance.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Abstract generic type behaviour in extended interface which redeclares method but does not use bound generic type (covariance)
+--FILE--
+<?php
+
+interface I<T> {
+    public function foo(int $param): T;
+}
+
+interface J<S> extends I<S> {
+    public function foo(int $param): int;
+}
+
+?>
+DONE
+--EXPECTF--
+Fatal error: Declaration of J<S : mixed>::foo(int $param): int must be compatible with I<T : <S>>::foo(int $param): <T : <S>> in %s on line %d
diff --git a/Zend/tests/type_declarations/abstract_generics/variance/multiple_abstract_generic_type-error.phpt b/Zend/tests/type_declarations/abstract_generics/variance/multiple_abstract_generic_type-error.phpt
new file mode 100644
index 0000000000000..9112b8fd11aaa
--- /dev/null
+++ b/Zend/tests/type_declarations/abstract_generics/variance/multiple_abstract_generic_type-error.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Multiple abstract generic type incorrect bound types in implementation
+--FILE--
+<?php
+
+interface I<K, V> {
+    public function set(K $key, V $value): void;
+    public function get(K $key): V;
+}
+
+class C implements I<string, int> {
+    public array $a = [];
+    public function set(int $key, string $value): void {
+        $this->a[$key] = $value . '!';
+    }
+    public function get(int $key): string {
+        return $this->a[$key];
+    }
+}
+
+?>
+--EXPECTF--
+Fatal error: Declaration of C::set(int $key, string $value): void must be compatible with I<K : string, V : int>::set(<K : string> $key, <V : int> $value): void in %s on line %d
diff --git a/Zend/zend.h b/Zend/zend.h
index 0cf1faeb653fe..d2f8d15c763b6 100644
--- a/Zend/zend.h
+++ b/Zend/zend.h
@@ -218,6 +218,12 @@ struct _zend_class_entry {
 	zend_trait_precedence **trait_precedences;
 	HashTable *attributes;
 
+	/* The bound_types HashTable is a map: "lower_case_interface_names" => map<int|string, zend_type>
+	 * Where an integer index refers to the position, and the string to the name of the generic parameter */
+	HashTable *bound_types;
+	zend_generic_parameter *generic_parameters;
+	uint32_t num_generic_parameters;
+
 	uint32_t enum_backing_type;
 	HashTable *backed_enum_table;
 
diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c
index 8bdd29c5512cc..5738109020d52 100644
--- a/Zend/zend_ast.c
+++ b/Zend/zend_ast.c
@@ -1570,6 +1570,16 @@ static ZEND_COLD void zend_ast_export_ns_name(smart_str *str, zend_ast *ast, int
 	zend_ast_export_ex(str, ast, priority, indent);
 }
 
+static ZEND_COLD void zend_ast_export_class_name(smart_str *str, zend_ast *ast, int priority, int indent)
+{
+	if (ast->kind == ZEND_AST_CLASS_REF) {
+		ZEND_ASSERT(ast->child[1] == NULL && "Generic params not supported yet");
+		zend_ast_export_ns_name(str, ast->child[0], priority, indent);
+		return;
+	}
+	zend_ast_export_ex(str, ast, priority, indent);
+}
+
 static ZEND_COLD bool zend_ast_valid_var_char(char ch)
 {
 	unsigned char c = (unsigned char)ch;
@@ -1690,7 +1700,7 @@ static ZEND_COLD void zend_ast_export_name_list_ex(smart_str *str, zend_ast_list
 		if (i != 0) {
 			smart_str_appends(str, separator);
 		}
-		zend_ast_export_name(str, list->child[i], 0, indent);
+		zend_ast_export_ns_name(str, list->child[i], 0, indent);
 		i++;
 	}
 }
@@ -1957,6 +1967,21 @@ static ZEND_COLD void zend_ast_export_type(smart_str *str, zend_ast *ast, int in
 	zend_ast_export_ns_name(str, ast, 0, indent);
 }
 
+static ZEND_COLD void zend_ast_export_generic_arg_list(smart_str *str, const zend_ast_list *list, int indent) {
+	// TODO Why cannot I just use
+	// zend_ast_export_list(str, list, true, 0, indent);
+	// ?
+
+	uint32_t i = 0;
+	while (i < list->children) {
+		if (i != 0) {
+			smart_str_appends(str, ", ");
+		}
+		zend_ast_export_type(str, list->child[i], indent);
+		i++;
+	}
+}
+
 static ZEND_COLD void zend_ast_export_hook_list(smart_str *str, zend_ast_list *hook_list, int indent)
 {
 	smart_str_appends(str, " {");
@@ -2156,10 +2181,17 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
 				}
 				smart_str_appends(str, "class ");
 			}
-			smart_str_appendl(str, ZSTR_VAL(decl->name), ZSTR_LEN(decl->name));
-			if (decl->flags & ZEND_ACC_ENUM && decl->child[4]) {
-				smart_str_appends(str, ": ");
-				zend_ast_export_type(str, decl->child[4], indent);
+			smart_str_append(str, decl->name);
+			if (decl->child[4]) {
+				if (decl->flags & ZEND_ACC_ENUM) {
+					smart_str_appends(str, ": ");
+					zend_ast_export_type(str, decl->child[4], indent);
+				} else {
+					ZEND_ASSERT(decl->flags & ZEND_ACC_INTERFACE);
+					smart_str_appendc(str, '<');
+					zend_ast_export_list(str, zend_ast_get_list(decl->child[4]), true, 0, indent);
+					smart_str_appendc(str, '>');
+				}
 			}
 			zend_ast_export_class_no_header(str, decl, indent);
 			smart_str_appendc(str, '\n');
@@ -2444,6 +2476,21 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
 			smart_str_appends(str, "::");
 			zend_ast_export_name(str, ast->child[1], 0, indent);
 			break;
+		case ZEND_AST_GENERIC_PARAM:
+			zend_ast_export_name(str, ast->child[0], 0, indent);
+			if (ast->child[1]) {
+				smart_str_appendl(str, ZEND_STRL(" : "));
+				zend_ast_export_type(str, ast->child[1], indent);
+			}
+			break;
+		case ZEND_AST_CLASS_REF:
+			zend_ast_export_ns_name(str, ast->child[0], 0, indent);
+			if (ast->child[1]) {
+				smart_str_appendc(str, '<');
+				zend_ast_export_generic_arg_list(str, zend_ast_get_list(ast->child[1]), indent);
+				smart_str_appendc(str, '>');
+			}
+			break;
 		case ZEND_AST_CLASS_NAME:
 			if (ast->child[0] == NULL) {
 				/* The const expr representation stores the fetch type instead. */
@@ -2457,7 +2504,7 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
 					EMPTY_SWITCH_DEFAULT_CASE()
 				}
 			} else {
-				zend_ast_export_ns_name(str, ast->child[0], 0, indent);
+				zend_ast_export_class_name(str, ast->child[0], 0, indent);
 			}
 			smart_str_appends(str, "::class");
 			break;
diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h
index 9348c35f6cc07..3b7d56ad54961 100644
--- a/Zend/zend_ast.h
+++ b/Zend/zend_ast.h
@@ -70,6 +70,8 @@ enum _zend_ast_kind {
 	ZEND_AST_ATTRIBUTE_GROUP,
 	ZEND_AST_MATCH_ARM_LIST,
 	ZEND_AST_MODIFIER_LIST,
+	ZEND_AST_GENERIC_PARAM_LIST,
+	ZEND_AST_GENERIC_ARG_LIST,
 
 	/* 0 child nodes */
 	ZEND_AST_MAGIC_CONST = 0 << ZEND_AST_NUM_CHILDREN_SHIFT,
@@ -154,6 +156,8 @@ enum _zend_ast_kind {
 	ZEND_AST_MATCH_ARM,
 	ZEND_AST_NAMED_ARG,
 	ZEND_AST_PARENT_PROPERTY_HOOK_CALL,
+	ZEND_AST_GENERIC_PARAM,
+	ZEND_AST_CLASS_REF,
 
 	/* 3 child nodes */
 	ZEND_AST_METHOD_CALL = 3 << ZEND_AST_NUM_CHILDREN_SHIFT,
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index 0669d106f15e9..f8733449da2cb 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -84,6 +84,8 @@ static inline uint32_t zend_alloc_cache_slot(void) {
 	return zend_alloc_cache_slots(1);
 }
 
+const zend_type zend_mixed_type = { NULL, MAY_BE_ANY, 0 };
+
 ZEND_API zend_op_array *(*zend_compile_file)(zend_file_handle *file_handle, int type);
 ZEND_API zend_op_array *(*zend_compile_string)(zend_string *source_string, const char *filename, zend_compile_position position);
 
@@ -1387,7 +1389,7 @@ static zend_string *add_type_string(zend_string *type, zend_string *new_type, bo
 	return result;
 }
 
-static zend_string *resolve_class_name(zend_string *name, zend_class_entry *scope) {
+static zend_string *resolve_class_name(zend_string *name, const zend_class_entry *scope) {
 	if (scope) {
 		if (zend_string_equals_ci(name, ZSTR_KNOWN(ZEND_STR_SELF))) {
 			name = scope->name;
@@ -1407,7 +1409,7 @@ static zend_string *resolve_class_name(zend_string *name, zend_class_entry *scop
 }
 
 static zend_string *add_intersection_type(zend_string *str,
-	const zend_type_list *intersection_type_list, zend_class_entry *scope,
+	const zend_type_list *intersection_type_list, const zend_class_entry *scope,
 	bool is_bracketed)
 {
 	const zend_type *single_type;
@@ -1432,7 +1434,40 @@ static zend_string *add_intersection_type(zend_string *str,
 	return str;
 }
 
-zend_string *zend_type_to_string_resolved(const zend_type type, zend_class_entry *scope) {
+static zend_string* resolve_bound_generic_type(const zend_type *type, const zend_class_entry *scope, const HashTable *bound_types) {
+	const zend_string *type_name = ZEND_TYPE_NAME(*type);
+	if (bound_types == NULL) {
+		const size_t len = ZSTR_LEN(type_name) + strlen("<>");
+		zend_string *result = zend_string_alloc(len, 0);
+		ZSTR_VAL(result)[0] = '<';
+		memcpy(ZSTR_VAL(result) + strlen("<"), ZSTR_VAL(type_name), ZSTR_LEN(type_name));
+		ZSTR_VAL(result)[len-1] = '>';
+		ZSTR_VAL(result)[len] = '\0';
+		return result;
+	}
+
+	const zend_type *constraint = zend_hash_index_find_ptr(bound_types, type->generic_param_index);
+	ZEND_ASSERT(constraint != NULL);
+
+	zend_string *constraint_type_str = zend_type_to_string_resolved(*constraint, scope, NULL);
+
+	size_t len = ZSTR_LEN(type_name) + ZSTR_LEN(constraint_type_str) + strlen("< : >");
+	zend_string *result = zend_string_alloc(len, 0);
+
+	ZSTR_VAL(result)[0] = '<';
+	memcpy(ZSTR_VAL(result) + strlen("<"), ZSTR_VAL(type_name), ZSTR_LEN(type_name));
+	ZSTR_VAL(result)[ZSTR_LEN(type_name) + 1] = ' ';
+	ZSTR_VAL(result)[ZSTR_LEN(type_name) + 2] = ':';
+	ZSTR_VAL(result)[ZSTR_LEN(type_name) + 3] = ' ';
+	memcpy(ZSTR_VAL(result) + ZSTR_LEN(type_name) + strlen("< : "), ZSTR_VAL(constraint_type_str), ZSTR_LEN(constraint_type_str));
+	ZSTR_VAL(result)[len-1] = '>';
+	ZSTR_VAL(result)[len] = '\0';
+
+	zend_string_release(constraint_type_str);
+	return result;
+}
+
+zend_string *zend_type_to_string_resolved(const zend_type type, const zend_class_entry *scope, const HashTable *bound_types_to_scope) {
 	zend_string *str = NULL;
 
 	/* Pure intersection type */
@@ -1455,6 +1490,8 @@ zend_string *zend_type_to_string_resolved(const zend_type type, zend_class_entry
 			str = add_type_string(str, resolved, /* is_intersection */ false);
 			zend_string_release(resolved);
 		} ZEND_TYPE_LIST_FOREACH_END();
+	} else if (ZEND_TYPE_IS_GENERIC_PARAM_NAME(type)) {
+		str = resolve_bound_generic_type(&type, scope, bound_types_to_scope);
 	} else if (ZEND_TYPE_HAS_NAME(type)) {
 		str = resolve_class_name(ZEND_TYPE_NAME(type), scope);
 	}
@@ -1524,7 +1561,7 @@ zend_string *zend_type_to_string_resolved(const zend_type type, zend_class_entry
 }
 
 ZEND_API zend_string *zend_type_to_string(zend_type type) {
-	return zend_type_to_string_resolved(type, NULL);
+	return zend_type_to_string_resolved(type, NULL, NULL);
 }
 
 static bool is_generator_compatible_class_type(const zend_string *name) {
@@ -1771,6 +1808,18 @@ static zend_string *zend_resolve_const_class_name_reference(zend_ast *ast, const
 	return zend_resolve_class_name(class_name, ast->attr);
 }
 
+static zend_string *zend_resolve_const_class_name_reference_with_generics(zend_ast *ast, const char *type)
+{
+	zend_ast *name_ast = ast->child[0];
+	zend_string *class_name = zend_ast_get_str(name_ast);
+	if (ZEND_FETCH_CLASS_DEFAULT != zend_get_class_fetch_type_ast(name_ast)) {
+		zend_error_noreturn(E_COMPILE_ERROR,
+			"Cannot use \"%s\" as %s, as it is reserved",
+			ZSTR_VAL(class_name), type);
+	}
+	return zend_resolve_class_name(class_name, name_ast->attr);
+}
+
 static void zend_ensure_valid_class_fetch_type(uint32_t fetch_type) /* {{{ */
 {
 	if (fetch_type != ZEND_FETCH_CLASS_DEFAULT && zend_is_scope_known()) {
@@ -2072,6 +2121,10 @@ ZEND_API void zend_initialize_class_data(zend_class_entry *ce, bool nullify_hand
 	ce->default_static_members_count = 0;
 	ce->properties_info_table = NULL;
 	ce->attributes = NULL;
+	ce->bound_types = NULL;
+	// TODO Should these be inside nullify_handlers?
+	ce->generic_parameters = NULL;
+	ce->num_generic_parameters = 0;
 	ce->enum_backing_type = IS_UNDEF;
 	ce->backed_enum_table = NULL;
 
@@ -6962,9 +7015,11 @@ ZEND_API void zend_set_function_arg_flags(zend_function *func) /* {{{ */
 
 static zend_type zend_compile_single_typename(zend_ast *ast)
 {
+	zend_class_entry *ce = CG(active_class_entry);
+
 	ZEND_ASSERT(!(ast->attr & ZEND_TYPE_NULLABLE));
 	if (ast->kind == ZEND_AST_TYPE) {
-		if (ast->attr == IS_STATIC && !CG(active_class_entry) && zend_is_scope_known()) {
+		if (ast->attr == IS_STATIC && !ce && zend_is_scope_known()) {
 			zend_error_noreturn(E_COMPILE_ERROR,
 				"Cannot use \"static\" when no class scope is active");
 		}
@@ -6993,8 +7048,17 @@ static zend_type zend_compile_single_typename(zend_ast *ast)
 		} else {
 			const char *correct_name;
 			uint32_t fetch_type = zend_get_class_fetch_type_ast(ast);
-			zend_string *class_name = type_name;
 
+			if (ce && ce->num_generic_parameters > 0) {
+				for (uint32_t generic_param_index = 0; generic_param_index < ce->num_generic_parameters; generic_param_index++) {
+					const zend_generic_parameter *generic_param = &ce->generic_parameters[generic_param_index];
+					if (zend_string_equals(type_name, generic_param->name)) {
+						return (zend_type) ZEND_TYPE_INIT_GENERIC_PARAM(zend_string_copy(type_name), generic_param_index);
+					}
+				}
+			}
+
+			zend_string *class_name = type_name;
 			if (fetch_type == ZEND_FETCH_CLASS_DEFAULT) {
 				class_name = zend_resolve_class_name_ast(ast);
 				zend_assert_valid_class_name(class_name, "a type name");
@@ -7005,14 +7069,14 @@ static zend_type zend_compile_single_typename(zend_ast *ast)
 				if (fetch_type == ZEND_FETCH_CLASS_SELF) {
 					/* Scope might be unknown for unbound closures and traits */
 					if (zend_is_scope_known()) {
-						class_name = CG(active_class_entry)->name;
+						class_name = ce->name;
 						ZEND_ASSERT(class_name && "must know class name when resolving self type at compile time");
 					}
 				} else {
 					ZEND_ASSERT(fetch_type == ZEND_FETCH_CLASS_PARENT);
 					/* Scope might be unknown for unbound closures and traits */
 					if (zend_is_scope_known()) {
-						class_name = CG(active_class_entry)->parent_name;
+						class_name = ce->parent_name;
 						ZEND_ASSERT(class_name && "must know class name when resolving parent type at compile time");
 					}
 				}
@@ -7189,6 +7253,9 @@ static zend_type zend_compile_typename_ex(
 			single_type = zend_compile_single_typename(type_ast);
 			uint32_t single_type_mask = ZEND_TYPE_PURE_MASK(single_type);
 
+			if (ZEND_TYPE_IS_GENERIC_PARAM_NAME(single_type)) {
+				zend_error_noreturn(E_COMPILE_ERROR, "Generic type cannot be part of a union type");
+			}
 			if (single_type_mask == MAY_BE_ANY) {
 				zend_error_noreturn(E_COMPILE_ERROR, "Type mixed can only be used as a standalone type");
 			}
@@ -7271,6 +7338,9 @@ static zend_type zend_compile_typename_ex(
 			zend_ast *type_ast = list->child[i];
 			zend_type single_type = zend_compile_single_typename(type_ast);
 
+			if (ZEND_TYPE_IS_GENERIC_PARAM_NAME(single_type)) {
+				zend_error_noreturn(E_COMPILE_ERROR, "Generic type cannot be part of an intersection type");
+			}
 			/* An intersection of union types cannot exist so invalidate it
 			 * Currently only can happen with iterable getting canonicalized to Traversable|array */
 			if (ZEND_TYPE_IS_ITERABLE_FALLBACK(single_type)) {
@@ -7337,6 +7407,12 @@ static zend_type zend_compile_typename_ex(
 	if ((type_mask & MAY_BE_NULL) && is_marked_nullable) {
 		zend_error_noreturn(E_COMPILE_ERROR, "null cannot be marked as nullable");
 	}
+	if (ZEND_TYPE_IS_GENERIC_PARAM_NAME(type) && is_marked_nullable) {
+		zend_error_noreturn(E_COMPILE_ERROR, "Generic type cannot be part of a union type");
+	}
+	if (ZEND_TYPE_IS_GENERIC_PARAM_NAME(type) && force_allow_null) {
+		zend_error_noreturn(E_COMPILE_ERROR, "Generic type cannot be part of a union type (implicitly nullable due to default null value)");
+	}
 
 	if (force_allow_null && !is_marked_nullable && !(type_mask & MAY_BE_NULL)) {
 		*forced_allow_null = true;
@@ -9011,20 +9087,52 @@ static void zend_compile_use_trait(zend_ast *ast) /* {{{ */
 }
 /* }}} */
 
+static void zend_bound_types_ht_dtor(zval *ptr) {
+	HashTable *interface_bound_types = Z_PTR_P(ptr);
+	zend_hash_destroy(interface_bound_types);
+	efree(interface_bound_types);
+}
+
+static void zend_types_ht_dtor(zval *ptr) {
+	zend_type *type = Z_PTR_P(ptr);
+	// TODO Figure out persistency?
+	zend_type_release(*type, false);
+	efree(type);
+}
+
 static void zend_compile_implements(zend_ast *ast) /* {{{ */
 {
 	zend_ast_list *list = zend_ast_get_list(ast);
 	zend_class_entry *ce = CG(active_class_entry);
 	zend_class_name *interface_names;
-	uint32_t i;
 
 	interface_names = emalloc(sizeof(zend_class_name) * list->children);
 
-	for (i = 0; i < list->children; ++i) {
-		zend_ast *class_ast = list->child[i];
+	for (uint32_t i = 0; i < list->children; ++i) {
+		zend_ast *interface_ast = list->child[i];
 		interface_names[i].name =
-			zend_resolve_const_class_name_reference(class_ast, "interface name");
+			zend_resolve_const_class_name_reference_with_generics(interface_ast, "interface name");
 		interface_names[i].lc_name = zend_string_tolower(interface_names[i].name);
+
+		if (interface_ast->child[1]) {
+			const zend_ast_list *generics_list = zend_ast_get_list(interface_ast->child[1]);
+			const uint32_t num_generic_args = generics_list->children;
+
+			// TODO Can we already check that we have correct number of generic args?
+			if (ce->bound_types == NULL) {
+				ALLOC_HASHTABLE(ce->bound_types);
+				zend_hash_init(ce->bound_types, list->children-i, NULL, zend_bound_types_ht_dtor, false /* todo depend on internal or not? */);
+			}
+
+			HashTable *bound_interface_types;
+			ALLOC_HASHTABLE(bound_interface_types);
+			zend_hash_init(bound_interface_types, num_generic_args, NULL, zend_types_ht_dtor, false /* todo depend on internal or not? */);
+			for (uint32_t generic_param = 0; generic_param < num_generic_args; ++generic_param) {
+				zend_type bound_type = zend_compile_typename(generics_list->child[generic_param]);
+				zend_hash_index_add_mem(bound_interface_types, generic_param, &bound_type, sizeof(bound_type));
+			}
+			zend_hash_add_new_ptr(ce->bound_types, interface_names[i].lc_name, bound_interface_types);
+		}
 	}
 
 	ce->num_interfaces = list->children;
@@ -9043,7 +9151,7 @@ static zend_string *zend_generate_anon_class_name(zend_ast_decl *decl)
 		prefix = zend_resolve_const_class_name_reference(decl->child[0], "class name");
 	} else if (decl->child[1]) {
 		zend_ast_list *list = zend_ast_get_list(decl->child[1]);
-		prefix = zend_resolve_const_class_name_reference(list->child[0], "interface name");
+		prefix = zend_resolve_const_class_name_reference_with_generics(list->child[0], "interface name");
 	}
 
 	zend_string *result = zend_strpprintf(0, "%s@anonymous%c%s:%" PRIu32 "$%" PRIx32,
@@ -9072,6 +9180,53 @@ static void zend_compile_enum_backing_type(zend_class_entry *ce, zend_ast *enum_
 	zend_type_release(type, 0);
 }
 
+
+static void zend_compile_generic_params(zend_ast *params_ast)
+{
+	const zend_ast_list *list = zend_ast_get_list(params_ast);
+	zend_generic_parameter *generic_params = safe_pemalloc(list->children, sizeof(zend_generic_parameter), 0, CG(active_class_entry)->type & ZEND_INTERNAL_CLASS);
+	CG(active_class_entry)->generic_parameters = generic_params;
+
+	for (uint32_t i = 0; i < list->children; i++) {
+		const zend_ast *param_ast = list->child[i];
+		zend_string *name = zend_ast_get_str(param_ast->child[0]);
+		zend_type constraint_type = zend_mixed_type;
+
+		if (zend_string_equals(name, CG(active_class_entry)->name)) {
+			zend_error_noreturn(E_COMPILE_ERROR,
+				"Generic parameter %s has same name as class", ZSTR_VAL(name));
+		}
+
+		for (uint32_t j = 0; j < i; j++) {
+			if (zend_string_equals(name, generic_params[j].name)) {
+				zend_error_noreturn(E_COMPILE_ERROR,
+					"Duplicate generic parameter %s", ZSTR_VAL(name));
+			}
+		}
+
+		if (param_ast->child[1]) {
+			constraint_type = zend_compile_typename(param_ast->child[1]);
+			if (ZEND_TYPE_IS_GENERIC_PARAM_NAME(constraint_type)) {
+				zend_error_noreturn(E_COMPILE_ERROR,
+					"Cannot use generic parameter %s to constrain generic parameter %s",
+					ZSTR_VAL(ZEND_TYPE_NAME(constraint_type)), ZSTR_VAL(name));
+			}
+			if (ZEND_TYPE_FULL_MASK(constraint_type) & (MAY_BE_STATIC|MAY_BE_VOID|MAY_BE_NEVER)) {
+				zend_error_noreturn(E_COMPILE_ERROR,
+					"Cannot use static, void, or never to constrain generic parameter %s",
+					ZSTR_VAL(name));
+			}
+		}
+
+		generic_params[i].name = zend_string_copy(name);
+		generic_params[i].constraint = constraint_type;
+
+		/* Update number of parameters on the fly, so that previous parameters can be
+		 * referenced in the type constraint of following parameters. */
+		CG(active_class_entry)->num_generic_parameters = i + 1;
+	}
+}
+
 static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) /* {{{ */
 {
 	zend_ast_decl *decl = (zend_ast_decl *) ast;
@@ -9166,6 +9321,10 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel)
 		zend_compile_attributes(&ce->attributes, decl->child[3], 0, ZEND_ATTRIBUTE_TARGET_CLASS, 0);
 	}
 
+	if (ce->ce_flags & ZEND_ACC_INTERFACE && decl->child[4]) {
+		zend_compile_generic_params(decl->child[4]);
+	}
+
 	if (implements_ast) {
 		zend_compile_implements(implements_ast);
 	}
diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h
index 62d0fbcded2ee..8e333127ce49a 100644
--- a/Zend/zend_compile.h
+++ b/Zend/zend_compile.h
@@ -53,6 +53,7 @@
 
 typedef struct _zend_op_array zend_op_array;
 typedef struct _zend_op zend_op;
+extern const zend_type zend_mixed_type;
 
 /* On 64-bit systems less optimal, but more compact VM code leads to better
  * performance. So on 32-bit systems we use absolute addresses for jump
@@ -1013,7 +1014,7 @@ int ZEND_FASTCALL zendlex(zend_parser_stack_elem *elem);
 
 void zend_assert_valid_class_name(const zend_string *const_name, const char *type);
 
-zend_string *zend_type_to_string_resolved(zend_type type, zend_class_entry *scope);
+zend_string *zend_type_to_string_resolved(zend_type type, const zend_class_entry *scope, const HashTable *bound_types_to_scope);
 ZEND_API zend_string *zend_type_to_string(zend_type type);
 
 /* BEGIN: OPCODES */
diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c
index 0fbfdfa07ef04..3c0679233556d 100644
--- a/Zend/zend_execute.c
+++ b/Zend/zend_execute.c
@@ -686,7 +686,7 @@ static ZEND_COLD void zend_verify_type_error_common(
 		*fclass = "";
 	}
 
-	*need_msg = zend_type_to_string_resolved(arg_info->type, zf->common.scope);
+	*need_msg = zend_type_to_string_resolved(arg_info->type, zf->common.scope, /* TODO? */ NULL);
 
 	if (value) {
 		*given_kind = zend_zval_value_name(value);
diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c
index d27cca5b76187..cf4c7528eb84b 100644
--- a/Zend/zend_inheritance.c
+++ b/Zend/zend_inheritance.c
@@ -60,8 +60,8 @@ static void add_property_hook_obligation(
 		zend_class_entry *ce, const zend_property_info *hooked_prop, const zend_function *hook_func);
 
 static void ZEND_COLD emit_incompatible_method_error(
-		const zend_function *child, zend_class_entry *child_scope,
-		const zend_function *parent, zend_class_entry *parent_scope,
+		const zend_function *child, const zend_class_entry *child_scope,
+		const zend_function *parent, const zend_class_entry *parent_scope,
 		inheritance_status status);
 
 static void zend_type_copy_ctor(zend_type *const type, bool use_arena, bool persistent);
@@ -91,7 +91,7 @@ static void zend_type_list_copy_ctor(
 static void zend_type_copy_ctor(zend_type *const type, bool use_arena, bool persistent) {
 	if (ZEND_TYPE_HAS_LIST(*type)) {
 		zend_type_list_copy_ctor(type, use_arena, persistent);
-	} else if (ZEND_TYPE_HAS_NAME(*type)) {
+	} else if (ZEND_TYPE_HAS_NAME(*type) || ZEND_TYPE_IS_GENERIC_PARAM_NAME(*type)) {
 		zend_string_addref(ZEND_TYPE_NAME(*type));
 	}
 }
@@ -428,16 +428,16 @@ static void track_class_dependency(zend_class_entry *ce, zend_string *class_name
 
 /* Check whether any type in the fe_type intersection type is a subtype of the proto class. */
 static inheritance_status zend_is_intersection_subtype_of_class(
-		zend_class_entry *fe_scope, const zend_type fe_type,
+		zend_class_entry *fe_scope, const zend_type *fe_type_ptr,
 		zend_class_entry *proto_scope, zend_string *proto_class_name, zend_class_entry *proto_ce)
 {
-	ZEND_ASSERT(ZEND_TYPE_IS_INTERSECTION(fe_type));
+	ZEND_ASSERT(ZEND_TYPE_IS_INTERSECTION(*fe_type_ptr));
 	bool have_unresolved = false;
 	const zend_type *single_type;
 
 	/* Traverse the list of child types and check that at least one is
 	 * a subtype of the parent type being checked */
-	ZEND_TYPE_FOREACH(fe_type, single_type) {
+	ZEND_TYPE_FOREACH(*fe_type_ptr, single_type) {
 		zend_class_entry *fe_ce;
 		zend_string *fe_class_name = NULL;
 		if (ZEND_TYPE_HAS_NAME(*single_type)) {
@@ -473,7 +473,9 @@ static inheritance_status zend_is_intersection_subtype_of_class(
 /* Check whether a single class proto type is a subtype of a potentially complex fe_type. */
 static inheritance_status zend_is_class_subtype_of_type(
 		zend_class_entry *fe_scope, zend_string *fe_class_name,
-		zend_class_entry *proto_scope, const zend_type proto_type) {
+		zend_class_entry *proto_scope, const zend_type *proto_type_ptr
+) {
+	const zend_type proto_type = *proto_type_ptr;
 	zend_class_entry *fe_ce = NULL;
 	bool have_unresolved = 0;
 
@@ -521,7 +523,7 @@ static inheritance_status zend_is_class_subtype_of_type(
 	ZEND_TYPE_FOREACH(proto_type, single_type) {
 		if (ZEND_TYPE_IS_INTERSECTION(*single_type)) {
 			inheritance_status subtype_status = zend_is_class_subtype_of_type(
-				fe_scope, fe_class_name, proto_scope, *single_type);
+				fe_scope, fe_class_name, proto_scope, single_type);
 
 			switch (subtype_status) {
 				case INHERITANCE_ERROR:
@@ -606,9 +608,11 @@ static void register_unresolved_classes(zend_class_entry *scope, const zend_type
 }
 
 static inheritance_status zend_is_intersection_subtype_of_type(
-	zend_class_entry *fe_scope, const zend_type fe_type,
-	zend_class_entry *proto_scope, const zend_type proto_type)
-{
+		zend_class_entry *fe_scope, const zend_type *fe_type_ptr,
+		zend_class_entry *proto_scope, const zend_type *proto_type_ptr
+) {
+	const zend_type fe_type = *fe_type_ptr;
+	const zend_type proto_type = *proto_type_ptr;
 	bool have_unresolved = false;
 	const zend_type *single_type;
 	uint32_t proto_type_mask = ZEND_TYPE_PURE_MASK(proto_type);
@@ -644,7 +648,7 @@ static inheritance_status zend_is_intersection_subtype_of_type(
 
 		if (ZEND_TYPE_IS_INTERSECTION(*single_type)) {
 			status = zend_is_intersection_subtype_of_type(
-				fe_scope, fe_type, proto_scope, *single_type);
+				fe_scope, fe_type_ptr, proto_scope, single_type);
 		} else {
 			zend_string *proto_class_name = get_class_from_type(proto_scope, *single_type);
 			if (!proto_class_name) {
@@ -653,7 +657,7 @@ static inheritance_status zend_is_intersection_subtype_of_type(
 
 			zend_class_entry *proto_ce = NULL;
 			status = zend_is_intersection_subtype_of_class(
-				fe_scope, fe_type, proto_scope, proto_class_name, proto_ce);
+				fe_scope, fe_type_ptr, proto_scope, proto_class_name, proto_ce);
 		}
 
 		if (status == early_exit_status) {
@@ -671,10 +675,60 @@ static inheritance_status zend_is_intersection_subtype_of_type(
 	return early_exit_status == INHERITANCE_ERROR ? INHERITANCE_SUCCESS : INHERITANCE_ERROR;
 }
 
-ZEND_API inheritance_status zend_perform_covariant_type_check(
-		zend_class_entry *fe_scope, const zend_type fe_type,
-		zend_class_entry *proto_scope, const zend_type proto_type)
+static inheritance_status zend_perform_covariant_type_check(
+		zend_class_entry *fe_scope, const zend_type *fe_type_ptr,
+		zend_class_entry *proto_scope, const zend_type *proto_type_ptr);
+
+static inheritance_status zend_perform_contravariant_type_check(
+		zend_class_entry *fe_scope, const zend_type *fe_type_ptr,
+		zend_class_entry *proto_scope, const zend_type *proto_type_ptr);
+
+static inheritance_status zend_is_type_subtype_of_generic_type(
+	zend_class_entry *concrete_scope,
+	const zend_type *concrete_type_ptr,
+	zend_class_entry *generic_type_scope,
+	const zend_type *generic_type_ptr
+) {
+	ZEND_ASSERT(concrete_scope->bound_types);
+	const zend_type generic_type = *generic_type_ptr;
+	const HashTable *bound_generic_types = zend_hash_find_ptr_lc(concrete_scope->bound_types, generic_type_scope->name);
+
+	ZEND_ASSERT(bound_generic_types && "Have generic type");
+	ZEND_ASSERT(ZEND_TYPE_IS_GENERIC_PARAM_NAME(generic_type));
+
+	const zend_type *bound_type_ptr = zend_hash_index_find_ptr(bound_generic_types, generic_type.generic_param_index);
+	ZEND_ASSERT(bound_type_ptr != NULL);
+	if (ZEND_TYPE_IS_GENERIC_PARAM_NAME(*bound_type_ptr)) {
+		const zend_type concrete_type = *concrete_type_ptr;
+		if (
+			ZEND_TYPE_IS_GENERIC_PARAM_NAME(concrete_type)
+			&& concrete_type.generic_param_index == bound_type_ptr->generic_param_index
+		) {
+			return INHERITANCE_SUCCESS;
+		} else {
+			return INHERITANCE_ERROR;
+		}
+	} else {
+		/* Generic type must be invariant */
+		const inheritance_status sub_type_status = zend_perform_covariant_type_check(
+			concrete_scope, concrete_type_ptr, generic_type_scope, bound_type_ptr);
+		const inheritance_status super_type_status = zend_perform_contravariant_type_check(
+			concrete_scope, concrete_type_ptr, generic_type_scope, bound_type_ptr);
+
+		if (sub_type_status != super_type_status) {
+			return INHERITANCE_ERROR;
+		} else {
+			return sub_type_status;
+		}
+	}
+}
+
+static inheritance_status zend_perform_covariant_type_check_no_generics(
+		zend_class_entry *fe_scope, const zend_type *fe_type_ptr,
+		zend_class_entry *proto_scope, const zend_type *proto_type_ptr)
 {
+	const zend_type fe_type = *fe_type_ptr;
+	const zend_type proto_type = *proto_type_ptr;
 	ZEND_ASSERT(ZEND_TYPE_IS_SET(fe_type) && ZEND_TYPE_IS_SET(proto_type));
 
 	/* Apart from void, everything is trivially covariant to the mixed type.
@@ -713,7 +767,7 @@ ZEND_API inheritance_status zend_perform_covariant_type_check(
 		early_exit_status =
 			ZEND_TYPE_IS_INTERSECTION(proto_type) ? INHERITANCE_ERROR : INHERITANCE_SUCCESS;
 		inheritance_status status = zend_is_intersection_subtype_of_type(
-			fe_scope, fe_type, proto_scope, proto_type);
+			fe_scope, fe_type_ptr, proto_scope, proto_type_ptr);
 
 		if (status == early_exit_status) {
 			return status;
@@ -733,7 +787,7 @@ ZEND_API inheritance_status zend_perform_covariant_type_check(
 			/* Union has an intersection type as it's member */
 			if (ZEND_TYPE_IS_INTERSECTION(*single_type)) {
 				status = zend_is_intersection_subtype_of_type(
-					fe_scope, *single_type, proto_scope, proto_type);
+					fe_scope, single_type, proto_scope, proto_type_ptr);
 			} else {
 				zend_string *fe_class_name = get_class_from_type(fe_scope, *single_type);
 				if (!fe_class_name) {
@@ -741,7 +795,7 @@ ZEND_API inheritance_status zend_perform_covariant_type_check(
 				}
 
 				status = zend_is_class_subtype_of_type(
-					fe_scope, fe_class_name, proto_scope, proto_type);
+					fe_scope, fe_class_name, proto_scope, proto_type_ptr);
 			}
 
 			if (status == early_exit_status) {
@@ -762,9 +816,71 @@ ZEND_API inheritance_status zend_perform_covariant_type_check(
 	return INHERITANCE_UNRESOLVED;
 }
 
+static inheritance_status zend_is_generic_type_subtype_of_generic_type(
+		const zend_class_entry *fe_scope, const zend_type *fe_type_ptr,
+		const zend_class_entry *proto_scope, const zend_type *proto_type_ptr
+) {
+	if (UNEXPECTED(!ZEND_TYPE_IS_GENERIC_PARAM_NAME(*proto_type_ptr))) {
+		/* A generic type cannot be a subtype of a concrete one */
+		return INHERITANCE_ERROR;
+	}
+	ZEND_ASSERT(ZEND_TYPE_IS_GENERIC_PARAM_NAME(*fe_type_ptr));
+	ZEND_ASSERT(fe_scope->bound_types);
+
+	const HashTable *bound_generic_types = zend_hash_find_ptr_lc(fe_scope->bound_types, proto_scope->name);
+	ZEND_ASSERT(bound_generic_types && "Must have bound generic type");
+
+	const zend_type *bound_type_ptr = zend_hash_index_find_ptr(bound_generic_types, proto_type_ptr->generic_param_index);
+	ZEND_ASSERT(bound_type_ptr != NULL);
+
+	const zend_type bound_type = *bound_type_ptr;
+	ZEND_ASSERT(ZEND_TYPE_IS_GENERIC_PARAM_NAME(bound_type));
+
+	return bound_type_ptr->generic_param_index == fe_type_ptr->generic_param_index ? INHERITANCE_SUCCESS : INHERITANCE_ERROR;
+}
+
+static inheritance_status zend_perform_covariant_type_check(
+		zend_class_entry *fe_scope, const zend_type *fe_type_ptr,
+		zend_class_entry *proto_scope, const zend_type *proto_type_ptr
+) {
+	/* If we check for concrete return type */
+	if (ZEND_TYPE_IS_GENERIC_PARAM_NAME(*proto_type_ptr)) {
+		return zend_is_type_subtype_of_generic_type(
+			fe_scope, fe_type_ptr, proto_scope, proto_type_ptr);
+	} else if (UNEXPECTED(ZEND_TYPE_IS_GENERIC_PARAM_NAME(*fe_type_ptr))) {
+		/* A generic type cannot be a subtype of a concrete one */
+		return INHERITANCE_ERROR;
+	}
+
+	return zend_perform_covariant_type_check_no_generics(
+			fe_scope, fe_type_ptr, proto_scope, proto_type_ptr);
+}
+
+static inheritance_status zend_perform_contravariant_type_check(
+		zend_class_entry *fe_scope, const zend_type *fe_type_ptr,
+		zend_class_entry *proto_scope, const zend_type *proto_type_ptr
+) {
+	const zend_type fe_type = *fe_type_ptr;
+	const zend_type proto_type = *proto_type_ptr;
+
+	if (ZEND_TYPE_IS_GENERIC_PARAM_NAME(fe_type)) {
+		return zend_is_generic_type_subtype_of_generic_type(
+			fe_scope, fe_type_ptr, proto_scope, proto_type_ptr);
+	}
+	if (ZEND_TYPE_IS_GENERIC_PARAM_NAME(proto_type)) {
+		return zend_is_type_subtype_of_generic_type(
+			fe_scope, fe_type_ptr, proto_scope, proto_type_ptr);
+	}
+
+	/* Contravariant type check is performed as a covariant type check with swapped
+	 * argument order. */
+	return zend_perform_covariant_type_check_no_generics(
+			proto_scope, proto_type_ptr, fe_scope, fe_type_ptr);
+}
+
 static inheritance_status zend_do_perform_arg_type_hint_check(
-		zend_class_entry *fe_scope, zend_arg_info *fe_arg_info,
-		zend_class_entry *proto_scope, zend_arg_info *proto_arg_info) /* {{{ */
+		zend_class_entry *fe_scope, const zend_arg_info *fe_arg_info,
+		zend_class_entry *proto_scope, const zend_arg_info *proto_arg_info) /* {{{ */
 {
 	if (!ZEND_TYPE_IS_SET(fe_arg_info->type) || ZEND_TYPE_PURE_MASK(fe_arg_info->type) == MAY_BE_ANY) {
 		/* Child with no type or mixed type is always compatible */
@@ -776,10 +892,8 @@ static inheritance_status zend_do_perform_arg_type_hint_check(
 		return INHERITANCE_ERROR;
 	}
 
-	/* Contravariant type check is performed as a covariant type check with swapped
-	 * argument order. */
-	return zend_perform_covariant_type_check(
-		proto_scope, proto_arg_info->type, fe_scope, fe_arg_info->type);
+	return zend_perform_contravariant_type_check(
+		fe_scope, &fe_arg_info->type, proto_scope, &proto_arg_info->type);
 }
 /* }}} */
 
@@ -832,10 +946,10 @@ static inheritance_status zend_do_perform_implementation_check(
 
 	status = INHERITANCE_SUCCESS;
 	for (uint32_t i = 0; i < num_args; i++) {
-		zend_arg_info *proto_arg_info =
+		const zend_arg_info *proto_arg_info =
 			i < proto_num_args ? &proto->common.arg_info[i] :
 			proto_is_variadic ? &proto->common.arg_info[proto_num_args - 1] : NULL;
-		zend_arg_info *fe_arg_info =
+		const zend_arg_info *fe_arg_info =
 			i < fe_num_args ? &fe->common.arg_info[i] :
 			fe_is_variadic ? &fe->common.arg_info[fe_num_args - 1] : NULL;
 		if (!proto_arg_info) {
@@ -881,7 +995,7 @@ static inheritance_status zend_do_perform_implementation_check(
 		}
 
 		local_status = zend_perform_covariant_type_check(
-			fe_scope, fe->common.arg_info[-1].type, proto_scope, proto->common.arg_info[-1].type);
+			fe_scope, &fe->common.arg_info[-1].type, proto_scope, &proto->common.arg_info[-1].type);
 
 		if (UNEXPECTED(local_status != INHERITANCE_SUCCESS)) {
 			if (local_status == INHERITANCE_ERROR
@@ -897,10 +1011,10 @@ static inheritance_status zend_do_perform_implementation_check(
 /* }}} */
 
 static ZEND_COLD void zend_append_type_hint(
-		smart_str *str, zend_class_entry *scope, const zend_arg_info *arg_info, bool return_hint) /* {{{ */
+		smart_str *str, const zend_class_entry *scope, const HashTable *bound_types_to_scope, const zend_arg_info *arg_info, bool return_hint) /* {{{ */
 {
 	if (ZEND_TYPE_IS_SET(arg_info->type)) {
-		zend_string *type_str = zend_type_to_string_resolved(arg_info->type, scope);
+		zend_string *type_str = zend_type_to_string_resolved(arg_info->type, scope, bound_types_to_scope);
 		smart_str_append(str, type_str);
 		zend_string_release(type_str);
 		if (!return_hint) {
@@ -911,7 +1025,7 @@ static ZEND_COLD void zend_append_type_hint(
 /* }}} */
 
 static ZEND_COLD zend_string *zend_get_function_declaration(
-		const zend_function *fptr, zend_class_entry *scope) /* {{{ */
+		const zend_function *fptr, const zend_class_entry *scope, const HashTable *bound_types_to_scope) /* {{{ */
 {
 	smart_str str = {0};
 
@@ -926,6 +1040,31 @@ static ZEND_COLD zend_string *zend_get_function_declaration(
 		} else {
 			smart_str_appendl(&str, ZSTR_VAL(fptr->common.scope->name), ZSTR_LEN(fptr->common.scope->name));
 		}
+		if (scope->num_generic_parameters) {
+			bool is_first = true;
+			smart_str_appendc(&str, '<');
+			for (uint32_t i = 0; i < scope->num_generic_parameters; i++) {
+				const zend_generic_parameter param = scope->generic_parameters[i];
+				zend_string *constraint_type_str;
+				if (bound_types_to_scope) {
+					const zend_type *constraint = zend_hash_index_find_ptr(bound_types_to_scope, i);
+					ZEND_ASSERT(constraint != NULL);
+					constraint_type_str = zend_type_to_string_resolved(*constraint, scope, NULL);
+				} else {
+					constraint_type_str = zend_type_to_string_resolved(param.constraint, scope, NULL);
+				}
+
+				if (!is_first) {
+					smart_str_appends(&str, ", ");
+				}
+				smart_str_append(&str, param.name);
+				smart_str_appends(&str, " : ");
+				smart_str_append(&str, constraint_type_str);
+				zend_string_release(constraint_type_str);
+				is_first = false;
+			}
+			smart_str_appendc(&str, '>');
+		}
 		smart_str_appends(&str, "::");
 	}
 
@@ -942,7 +1081,7 @@ static ZEND_COLD zend_string *zend_get_function_declaration(
 			num_args++;
 		}
 		for (uint32_t i = 0; i < num_args;) {
-			zend_append_type_hint(&str, scope, arg_info, 0);
+			zend_append_type_hint(&str, scope, bound_types_to_scope, arg_info, 0);
 
 			if (ZEND_ARG_SEND_MODE(arg_info)) {
 				smart_str_appendc(&str, '&');
@@ -1039,7 +1178,7 @@ static ZEND_COLD zend_string *zend_get_function_declaration(
 
 	if (fptr->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
 		smart_str_appends(&str, ": ");
-		zend_append_type_hint(&str, scope, fptr->common.arg_info - 1, 1);
+		zend_append_type_hint(&str, scope, bound_types_to_scope, fptr->common.arg_info - 1, 1);
 	}
 	smart_str_0(&str);
 
@@ -1056,11 +1195,15 @@ static zend_always_inline uint32_t func_lineno(const zend_function *fn) {
 }
 
 static void ZEND_COLD emit_incompatible_method_error(
-		const zend_function *child, zend_class_entry *child_scope,
-		const zend_function *parent, zend_class_entry *parent_scope,
+		const zend_function *child, const zend_class_entry *child_scope,
+		const zend_function *parent, const zend_class_entry *parent_scope,
 		inheritance_status status) {
-	zend_string *parent_prototype = zend_get_function_declaration(parent, parent_scope);
-	zend_string *child_prototype = zend_get_function_declaration(child, child_scope);
+	const HashTable *bound_types_to_parent = NULL;
+	if (child_scope->bound_types) {
+		bound_types_to_parent = zend_hash_find_ptr_lc(child_scope->bound_types, parent_scope->name);
+	}
+	zend_string *parent_prototype = zend_get_function_declaration(parent, parent_scope, bound_types_to_parent);
+	zend_string *child_prototype = zend_get_function_declaration(child, child_scope, NULL);
 	if (status == INHERITANCE_UNRESOLVED) {
 		// TODO Improve error message if first unresolved class is present in child and parent?
 		/* Fetch the first unresolved class from registered autoloads */
@@ -1297,10 +1440,10 @@ static inheritance_status full_property_types_compatible(
 	/* Perform a covariant type check in both directions to determined invariance. */
 	inheritance_status status1 = variance == PROP_CONTRAVARIANT ? INHERITANCE_SUCCESS :
 		zend_perform_covariant_type_check(
-			child_info->ce, child_info->type, parent_info->ce, parent_info->type);
+			child_info->ce, &child_info->type, parent_info->ce, &parent_info->type);
 	inheritance_status status2 = variance == PROP_COVARIANT ? INHERITANCE_SUCCESS :
-		zend_perform_covariant_type_check(
-			parent_info->ce, parent_info->type, child_info->ce, child_info->type);
+		zend_perform_contravariant_type_check(
+			child_info->ce, &child_info->type, parent_info->ce, &parent_info->type);
 	if (status1 == INHERITANCE_SUCCESS && status2 == INHERITANCE_SUCCESS) {
 		return INHERITANCE_SUCCESS;
 	}
@@ -1313,7 +1456,7 @@ static inheritance_status full_property_types_compatible(
 
 static ZEND_COLD void emit_incompatible_property_error(
 		const zend_property_info *child, const zend_property_info *parent, prop_variance variance) {
-	zend_string *type_str = zend_type_to_string_resolved(parent->type, parent->ce);
+	zend_string *type_str = zend_type_to_string_resolved(parent->type, parent->ce, /* TODO? */ NULL);
 	zend_error_noreturn(E_COMPILE_ERROR,
 		"Type of %s::$%s must be %s%s (as in class %s)",
 		ZSTR_VAL(child->ce->name),
@@ -1327,7 +1470,7 @@ static ZEND_COLD void emit_incompatible_property_error(
 static ZEND_COLD void emit_set_hook_type_error(const zend_property_info *child, const zend_property_info *parent)
 {
 	zend_type set_type = parent->hooks[ZEND_PROPERTY_HOOK_SET]->common.arg_info[0].type;
-	zend_string *type_str = zend_type_to_string_resolved(set_type, parent->ce);
+	zend_string *type_str = zend_type_to_string_resolved(set_type, parent->ce, /* TODO? */ NULL);
 	zend_error_noreturn(E_COMPILE_ERROR,
 		"Set type of %s::$%s must be supertype of %s (as in %s %s)",
 		ZSTR_VAL(child->ce->name),
@@ -1356,8 +1499,8 @@ static inheritance_status verify_property_type_compatibility(
 		if (parent_info->hooks[ZEND_PROPERTY_HOOK_SET]
 		 && (!child_info->hooks || !child_info->hooks[ZEND_PROPERTY_HOOK_SET])) {
 			zend_type set_type = parent_info->hooks[ZEND_PROPERTY_HOOK_SET]->common.arg_info[0].type;
-			inheritance_status result = zend_perform_covariant_type_check(
-				parent_info->ce, set_type, child_info->ce, child_info->type);
+			inheritance_status result = zend_perform_contravariant_type_check(
+				child_info->ce, &child_info->type, parent_info->ce, &set_type);
 			if ((result == INHERITANCE_ERROR && throw_on_error) || (result == INHERITANCE_UNRESOLVED && throw_on_unresolved)) {
 				emit_set_hook_type_error(child_info, parent_info);
 			}
@@ -1581,17 +1724,30 @@ static void do_inherit_property(zend_property_info *parent_info, zend_string *ke
 }
 /* }}} */
 
-static inline void do_implement_interface(zend_class_entry *ce, zend_class_entry *iface) /* {{{ */
+static inline void do_implement_interface_ex(zend_class_entry *ce, zend_class_entry *inherited_face, zend_class_entry *base_iface)
 {
-	if (!(ce->ce_flags & ZEND_ACC_INTERFACE) && iface->interface_gets_implemented && iface->interface_gets_implemented(iface, ce) == FAILURE) {
-		zend_error_noreturn(E_CORE_ERROR, "%s %s could not implement interface %s", zend_get_object_type_uc(ce), ZSTR_VAL(ce->name), ZSTR_VAL(iface->name));
+	if (!(ce->ce_flags & ZEND_ACC_INTERFACE) && inherited_face->interface_gets_implemented && inherited_face->interface_gets_implemented(base_iface, ce) == FAILURE) {
+		zend_error_noreturn(E_CORE_ERROR, "%s %s could not implement interface %s", zend_get_object_type_uc(ce), ZSTR_VAL(ce->name), ZSTR_VAL(base_iface->name));
 	}
 	/* This should be prevented by the class lookup logic. */
-	ZEND_ASSERT(ce != iface);
+	ZEND_ASSERT(ce != base_iface);
 }
-/* }}} */
 
-static void zend_do_inherit_interfaces(zend_class_entry *ce, const zend_class_entry *iface) /* {{{ */
+static inline void do_implement_interface(zend_class_entry *ce, zend_class_entry *iface)
+{
+	do_implement_interface_ex(ce, iface, iface);
+}
+
+static ZEND_COLD void emit_incompatible_generic_arg_count_error(const zend_class_entry *iface, uint32_t given_args) {
+	zend_error_noreturn(E_COMPILE_ERROR,
+		"Interface %s expects %" PRIu32 " generic parameters, %" PRIu32 " given",
+		ZSTR_VAL(iface->name),
+		iface->num_generic_parameters,
+		given_args
+	);
+}
+
+static void zend_do_inherit_interfaces(zend_class_entry *ce, zend_class_entry *iface) /* {{{ */
 {
 	/* expects interface to be contained in ce's interface list already */
 	uint32_t i, ce_num, if_num = iface->num_interfaces;
@@ -1609,6 +1765,19 @@ static void zend_do_inherit_interfaces(zend_class_entry *ce, const zend_class_en
 		zend_class_entry *entry = iface->interfaces[if_num];
 		for (i = 0; i < ce_num; i++) {
 			if (ce->interfaces[i] == entry) {
+				if (entry->num_generic_parameters) {
+					if (UNEXPECTED(ce->bound_types == NULL)) {
+						emit_incompatible_generic_arg_count_error(entry, 0);
+					}
+					const HashTable *bound_types = zend_hash_find_ptr_lc(ce->bound_types, entry->name);
+					if (UNEXPECTED(bound_types == NULL)) {
+						emit_incompatible_generic_arg_count_error(entry, 0);
+					}
+					const uint32_t num_bound_types = zend_hash_num_elements(bound_types);
+					if (UNEXPECTED(num_bound_types != entry->num_generic_parameters)) {
+						emit_incompatible_generic_arg_count_error(entry, num_bound_types);
+					}
+				}
 				break;
 			}
 		}
@@ -1620,14 +1789,14 @@ static void zend_do_inherit_interfaces(zend_class_entry *ce, const zend_class_en
 
 	/* and now call the implementing handlers */
 	while (ce_num < ce->num_interfaces) {
-		do_implement_interface(ce, ce->interfaces[ce_num++]);
+		do_implement_interface_ex(ce, ce->interfaces[ce_num++], iface);
 	}
 }
 /* }}} */
 
 static void emit_incompatible_class_constant_error(
 		const zend_class_constant *child, const zend_class_constant *parent, const zend_string *const_name) {
-	zend_string *type_str = zend_type_to_string_resolved(parent->type, parent->ce);
+	zend_string *type_str = zend_type_to_string_resolved(parent->type, parent->ce, NULL);
 	zend_error_noreturn(E_COMPILE_ERROR,
 		"Type of %s::%s must be compatible with %s::%s of type %s",
 		ZSTR_VAL(child->ce->name),
@@ -1645,7 +1814,7 @@ static inheritance_status class_constant_types_compatible(const zend_class_const
 		return INHERITANCE_ERROR;
 	}
 
-	return zend_perform_covariant_type_check(child->ce, child->type, parent->ce, parent->type);
+	return zend_perform_covariant_type_check(child->ce, &child->type, parent->ce, &parent->type);
 }
 
 static bool do_inherit_constant_check(
@@ -1791,7 +1960,7 @@ ZEND_API inheritance_status zend_verify_property_hook_variance(const zend_proper
 {
 	ZEND_ASSERT(prop_info->hooks && prop_info->hooks[ZEND_PROPERTY_HOOK_SET] == func);
 
-	zend_arg_info *value_arg_info = &func->op_array.arg_info[0];
+	const zend_arg_info *value_arg_info = &func->op_array.arg_info[0];
 	if (!ZEND_TYPE_IS_SET(value_arg_info->type)) {
 		return INHERITANCE_SUCCESS;
 	}
@@ -1801,7 +1970,7 @@ ZEND_API inheritance_status zend_verify_property_hook_variance(const zend_proper
 	}
 
 	zend_class_entry *ce = prop_info->ce;
-	return zend_perform_covariant_type_check(ce, prop_info->type, ce, value_arg_info->type);
+	return zend_perform_covariant_type_check(ce, &prop_info->type, ce, &value_arg_info->type);
 }
 
 #ifdef ZEND_OPCACHE_SHM_REATTACHMENT
@@ -2148,6 +2317,113 @@ static void do_inherit_iface_constant(zend_string *name, zend_class_constant *c,
 }
 /* }}} */
 
+// TODO Merge with the ones in zend_compile
+static void zend_bound_types_ht_dtor(zval *ptr) {
+	HashTable *interface_bound_types = Z_PTR_P(ptr);
+	zend_hash_destroy(interface_bound_types);
+	efree(interface_bound_types);
+}
+static void zend_types_ht_dtor(zval *ptr) {
+	zend_type *type = Z_PTR_P(ptr);
+	// TODO Figure out persistency?
+	zend_type_release(*type, false);
+	efree(type);
+}
+
+ZEND_ATTRIBUTE_NONNULL static void bind_generic_types_for_inherited_interfaces(zend_class_entry *ce, const zend_class_entry *iface) {
+	const HashTable *iface_bound_types = iface->bound_types;
+	if (iface_bound_types == NULL) {
+#ifdef ZEND_DEBUG
+		for (uint32_t i = 0; i < iface->num_interfaces; i++) {
+			const zend_class_entry *inherited_iface = iface->interfaces[i];
+			ZEND_ASSERT(inherited_iface->num_generic_parameters == 0);
+		}
+#endif
+		return;
+	}
+
+	if (ce->bound_types == NULL) {
+		ALLOC_HASHTABLE(ce->bound_types);
+		zend_hash_init(ce->bound_types, zend_hash_num_elements(iface_bound_types), NULL, zend_bound_types_ht_dtor, false /* todo depend on internal or not */);
+	}
+	const HashTable *ce_bound_types_for_direct_iface = zend_hash_find_ptr_lc(ce->bound_types, iface->name);
+
+	zend_string *lc_inherited_iface_name = NULL;
+	const HashTable *interface_bound_types_for_inherited_iface = NULL;
+	ZEND_HASH_FOREACH_STR_KEY_PTR(iface_bound_types, lc_inherited_iface_name, interface_bound_types_for_inherited_iface) {
+		ZEND_ASSERT(lc_inherited_iface_name != NULL);
+
+		const HashTable *existing_bound_types_for_inherited_iface = zend_hash_find_ptr(ce->bound_types, lc_inherited_iface_name);
+		if (EXPECTED(existing_bound_types_for_inherited_iface == NULL)) {
+			HashTable *ce_bound_types_for_inherited_iface = NULL;
+			ALLOC_HASHTABLE(ce_bound_types_for_inherited_iface);
+			zend_hash_init(
+				ce_bound_types_for_inherited_iface,
+				zend_hash_num_elements(interface_bound_types_for_inherited_iface),
+				NULL,
+				zend_types_ht_dtor,
+				false /* TODO depends on internals */
+			);
+
+			zend_ulong generic_param_index = 0;
+			const zend_type *bound_type_ptr = NULL;
+			ZEND_HASH_FOREACH_NUM_KEY_PTR(interface_bound_types_for_inherited_iface, generic_param_index, bound_type_ptr) {
+				zend_type bound_type = *bound_type_ptr;
+				if (ZEND_TYPE_IS_GENERIC_PARAM_NAME(bound_type)) {
+					ZEND_ASSERT(ce_bound_types_for_direct_iface != NULL &&
+						"If a bound type is generic then we must have bound types for the current interface");
+					const zend_type *ce_bound_type_ptr = zend_hash_index_find_ptr(ce_bound_types_for_direct_iface, bound_type_ptr->generic_param_index);
+					ZEND_ASSERT(ce_bound_type_ptr != NULL);
+					bound_type = *ce_bound_type_ptr;
+				}
+
+				zend_type_copy_ctor(&bound_type, true, false /* TODO Depends on internal or not? */);
+				zend_hash_index_add_mem(ce_bound_types_for_inherited_iface, generic_param_index,
+					&bound_type, sizeof(bound_type));
+			} ZEND_HASH_FOREACH_END();
+			zend_hash_add_new_ptr(ce->bound_types, lc_inherited_iface_name, ce_bound_types_for_inherited_iface);
+		} else {
+			const uint32_t num_generic_types = zend_hash_num_elements(interface_bound_types_for_inherited_iface);
+			ZEND_ASSERT(zend_hash_num_elements(existing_bound_types_for_inherited_iface) == num_generic_types && "Existing bound types should have errored before");
+
+			for (zend_ulong bound_type_index = 0; bound_type_index < num_generic_types; bound_type_index++) {
+				const zend_type *iface_bound_type_ptr = zend_hash_index_find_ptr(interface_bound_types_for_inherited_iface, bound_type_index);
+				const zend_type *ce_bound_type_ptr = zend_hash_index_find_ptr(existing_bound_types_for_inherited_iface, bound_type_index);
+				ZEND_ASSERT(iface_bound_type_ptr != NULL && ce_bound_type_ptr != NULL);
+				if (ZEND_TYPE_IS_GENERIC_PARAM_NAME(*iface_bound_type_ptr)) {
+					iface_bound_type_ptr = zend_hash_index_find_ptr(ce_bound_types_for_direct_iface, iface_bound_type_ptr->generic_param_index);
+					ZEND_ASSERT(iface_bound_type_ptr != NULL);
+				}
+				const zend_type t1 = *iface_bound_type_ptr;
+				const zend_type t2 = *ce_bound_type_ptr;
+				if (
+					ZEND_TYPE_FULL_MASK(t1) != ZEND_TYPE_FULL_MASK(t2)
+					|| (ZEND_TYPE_HAS_NAME(t1) && !zend_string_equals(ZEND_TYPE_NAME(t1), ZEND_TYPE_NAME(t2)))
+					// || ZEND_TYPE_HAS_LIST(t1) && TODO Check list types are equal
+				) {
+					const zend_class_entry *inherited_iface = zend_hash_find_ptr(CG(class_table), lc_inherited_iface_name);
+					ZEND_ASSERT(inherited_iface != NULL);
+					const zend_generic_parameter param = inherited_iface->generic_parameters[bound_type_index];
+
+					zend_string *ce_bound_type_str = zend_type_to_string_resolved(t2, ce, NULL);
+					zend_string *iface_bound_type_str = zend_type_to_string_resolved(t1, iface, NULL);
+					zend_error_noreturn(E_COMPILE_ERROR,
+						"Bound type %s for interface %s implemented explicitly in %s with type %s must match the implicitly bound type %s from interface %s",
+						ZSTR_VAL(param.name),
+						ZSTR_VAL(inherited_iface->name),
+						ZSTR_VAL(ce->name),
+						ZSTR_VAL(ce_bound_type_str),
+						ZSTR_VAL(iface_bound_type_str),
+						ZSTR_VAL(iface->name)
+					);
+					zend_string_release_ex(ce_bound_type_str, false);
+					zend_string_release_ex(iface_bound_type_str, false);
+				}
+			}
+		}
+	} ZEND_HASH_FOREACH_END();
+}
+
 static void do_interface_implementation(zend_class_entry *ce, zend_class_entry *iface) /* {{{ */
 {
 	zend_function *func;
@@ -2155,6 +2431,10 @@ static void do_interface_implementation(zend_class_entry *ce, zend_class_entry *
 	zend_class_constant *c;
 	uint32_t flags = ZEND_INHERITANCE_CHECK_PROTO | ZEND_INHERITANCE_CHECK_VISIBILITY;
 
+	if (iface->num_interfaces) {
+		zend_do_inherit_interfaces(ce, iface);
+	}
+
 	if (!(ce->ce_flags & ZEND_ACC_INTERFACE)) {
 		/* We are not setting the prototype of overridden interface methods because of abstract
 		 * constructors. See Zend/tests/interface_constructor_prototype_001.phpt. */
@@ -2168,6 +2448,80 @@ static void do_interface_implementation(zend_class_entry *ce, zend_class_entry *
 			ZEND_INHERITANCE_RESET_CHILD_OVERRIDE;
 	}
 
+	if (iface->num_generic_parameters > 0) {
+		if (UNEXPECTED(ce->bound_types == NULL)) {
+			emit_incompatible_generic_arg_count_error(iface, 0);
+		}
+		HashTable *bound_types = zend_hash_find_ptr_lc(ce->bound_types, iface->name);
+		if (UNEXPECTED(bound_types == NULL)) {
+			emit_incompatible_generic_arg_count_error(iface, 0);
+		}
+		const uint32_t num_bound_types = zend_hash_num_elements(bound_types);
+		if (UNEXPECTED(num_bound_types != iface->num_generic_parameters)) {
+			emit_incompatible_generic_arg_count_error(iface, num_bound_types);
+		}
+		for (uint32_t i = 0; i < num_bound_types; i++) {
+			const zend_generic_parameter *generic_parameter = &iface->generic_parameters[i];
+			const zend_type* generic_constraint = &generic_parameter->constraint;
+			zend_type *bound_type_ptr = zend_hash_index_find_ptr(bound_types, i);
+			ZEND_ASSERT(bound_type_ptr != NULL);
+
+			/* We are currently extending another interface */
+			if (ZEND_TYPE_IS_GENERIC_PARAM_NAME(*bound_type_ptr)) {
+				ZEND_ASSERT(ce->ce_flags & ZEND_ACC_INTERFACE);
+				ZEND_ASSERT(ce->num_generic_parameters > 0);
+				const zend_string *current_generic_param_name = ZEND_TYPE_NAME(*bound_type_ptr);
+				for (uint32_t j = 0; j < ce->num_generic_parameters; j++) {
+					const zend_generic_parameter *current_ce_generic_parameter = &ce->generic_parameters[j];
+					if (!zend_string_equals(current_ce_generic_parameter->name, current_generic_param_name)) {
+						continue;
+					}
+					const zend_type *current_ce_generic_type_constraint = &current_ce_generic_parameter->constraint;
+					ZEND_ASSERT(current_ce_generic_type_constraint != NULL);
+					if (
+						zend_perform_covariant_type_check(
+							ce,
+							current_ce_generic_type_constraint,
+							iface,
+							generic_constraint
+						) != INHERITANCE_SUCCESS
+					) {
+						zend_string *current_ce_constraint_type_str = zend_type_to_string(*current_ce_generic_type_constraint);
+						zend_string *constraint_type_str = zend_type_to_string(generic_parameter->constraint);
+						zend_error_noreturn(E_COMPILE_ERROR,
+							"Constraint type %s of generic type %s of interface %s is not a subtype of the constraint type %s of generic type %s of interface %s",
+							ZSTR_VAL(current_ce_constraint_type_str),
+							ZSTR_VAL(current_ce_generic_parameter->name),
+							ZSTR_VAL(ce->name),
+							ZSTR_VAL(constraint_type_str),
+							ZSTR_VAL(generic_parameter->name),
+							ZSTR_VAL(iface->name)
+						);
+						zend_string_release(current_ce_constraint_type_str);
+						zend_string_release(constraint_type_str);
+						return;
+					}
+					break;
+				}
+			} else {
+				if (zend_perform_covariant_type_check(ce, bound_type_ptr, iface, generic_constraint) != INHERITANCE_SUCCESS) {
+					zend_string *bound_type_str = zend_type_to_string(*bound_type_ptr);
+					zend_string *constraint_type_str = zend_type_to_string(generic_parameter->constraint);
+					zend_error_noreturn(E_COMPILE_ERROR,
+						"Bound type %s is not a subtype of the constraint type %s of generic type %s of interface %s",
+						ZSTR_VAL(bound_type_str),
+						ZSTR_VAL(constraint_type_str),
+						ZSTR_VAL(generic_parameter->name),
+						ZSTR_VAL(iface->name)
+					);
+					zend_string_release(bound_type_str);
+					zend_string_release(constraint_type_str);
+					return;
+				}
+			}
+		}
+	}
+	bind_generic_types_for_inherited_interfaces(ce, iface);
 	ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&iface->constants_table, key, c) {
 		do_inherit_iface_constant(key, c, ce, iface);
 	} ZEND_HASH_FOREACH_END();
@@ -2186,9 +2540,6 @@ static void do_interface_implementation(zend_class_entry *ce, zend_class_entry *
 	} ZEND_HASH_FOREACH_END();
 
 	do_implement_interface(ce, iface);
-	if (iface->num_interfaces) {
-		zend_do_inherit_interfaces(ce, iface);
-	}
 }
 /* }}} */
 
@@ -2777,8 +3128,8 @@ static bool do_trait_constant_check(
 		emit_incompatible_trait_constant_error(ce, existing_constant, trait_constant, name, traits, current_trait);
 		return false;
 	} else if (ZEND_TYPE_IS_SET(trait_constant->type)) {
-		inheritance_status status1 = zend_perform_covariant_type_check(ce, existing_constant->type, traits[current_trait], trait_constant->type);
-		inheritance_status status2 = zend_perform_covariant_type_check(traits[current_trait], trait_constant->type, ce, existing_constant->type);
+		inheritance_status status1 = zend_perform_covariant_type_check(ce, &existing_constant->type, traits[current_trait], &trait_constant->type);
+		inheritance_status status2 = zend_perform_contravariant_type_check(ce, &existing_constant->type, traits[current_trait], &trait_constant->type);
 		if (status1 == INHERITANCE_ERROR || status2 == INHERITANCE_ERROR) {
 			emit_incompatible_trait_constant_error(ce, existing_constant, trait_constant, name, traits, current_trait);
 			return false;
diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y
index 08b2ac6b3f39b..48d9b6e928b5e 100644
--- a/Zend/zend_language_parser.y
+++ b/Zend/zend_language_parser.y
@@ -261,7 +261,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
 %type <ast> static_var class_statement trait_adaptation trait_precedence trait_alias
 %type <ast> absolute_trait_method_reference trait_method_reference property echo_expr
 %type <ast> new_dereferenceable new_non_dereferenceable anonymous_class class_name class_name_reference simple_variable
-%type <ast> internal_functions_in_yacc
+%type <ast> internal_functions_in_yacc simple_class_name generic_arg_list
 %type <ast> scalar backticks_expr lexical_var function_call member_name property_name
 %type <ast> variable_class_name dereferenceable_scalar constant class_constant
 %type <ast> fully_dereferenceable array_object_dereferenceable
@@ -286,6 +286,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
 %type <ast> function_name non_empty_member_modifiers
 %type <ast> property_hook property_hook_list optional_property_hook_list hooked_property property_hook_body
 %type <ast> optional_parameter_list
+%type <ast> optional_generic_params generic_params generic_param class_name_with_generics_list
 
 %type <num> returns_ref function fn is_reference is_variadic property_modifiers property_hook_modifiers
 %type <num> method_modifiers class_const_modifiers member_modifier optional_cpp_modifiers
@@ -363,9 +364,9 @@ name:
 ;
 
 attribute_decl:
-		class_name
+		simple_class_name
 			{ $$ = zend_ast_create(ZEND_AST_ATTRIBUTE, $1, NULL); }
-	|	class_name argument_list
+	|	simple_class_name argument_list
 			{ $$ = zend_ast_create(ZEND_AST_ATTRIBUTE, $1, $2); }
 ;
 
@@ -550,8 +551,8 @@ catch_list:
 ;
 
 catch_name_list:
-		class_name { $$ = zend_ast_create_list(1, ZEND_AST_NAME_LIST, $1); }
-	|	catch_name_list '|' class_name { $$ = zend_ast_list_add($1, $3); }
+		simple_class_name { $$ = zend_ast_create_list(1, ZEND_AST_NAME_LIST, $1); }
+	|	catch_name_list '|' simple_class_name { $$ = zend_ast_list_add($1, $3); }
 ;
 
 optional_variable:
@@ -640,8 +641,8 @@ trait_declaration_statement:
 
 interface_declaration_statement:
 		T_INTERFACE { $<num>$ = CG(zend_lineno); }
-		T_STRING interface_extends_list backup_doc_comment '{' class_statement_list '}'
-			{ $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_INTERFACE, $<num>2, $5, zend_ast_get_str($3), NULL, $4, $7, NULL, NULL); }
+		T_STRING optional_generic_params interface_extends_list backup_doc_comment '{' class_statement_list '}'
+			{ $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_INTERFACE, $<num>2, $6, zend_ast_get_str($3), NULL, $5, $8, NULL, $4); }
 ;
 
 enum_declaration_statement:
@@ -665,19 +666,38 @@ enum_case_expr:
 	|	'=' expr { $$ = $2; }
 ;
 
+optional_generic_params:
+		%empty					{ $$ = NULL; }
+	|	'<' generic_params '>'	{ $$ = $2; }
+;
+
+generic_params:
+		generic_param
+			{ $$ = zend_ast_create_list(1, ZEND_AST_GENERIC_PARAM_LIST, $1); }
+	|	generic_params ',' generic_param
+			{ $$ = zend_ast_list_add($1, $3); }
+;
+
+generic_param:
+		T_STRING
+		    { $$ = zend_ast_create(ZEND_AST_GENERIC_PARAM, $1, NULL); }
+	|	T_STRING ':' type_expr
+	        { $$ = zend_ast_create(ZEND_AST_GENERIC_PARAM, $1, $3); }
+;
+
 extends_from:
 		%empty				{ $$ = NULL; }
-	|	T_EXTENDS class_name	{ $$ = $2; }
+	|	T_EXTENDS simple_class_name	{ $$ = $2; }
 ;
 
 interface_extends_list:
 		%empty			        { $$ = NULL; }
-	|	T_EXTENDS class_name_list	{ $$ = $2; }
+	|	T_EXTENDS class_name_with_generics_list	{ $$ = $2; }
 ;
 
 implements_list:
 		%empty		        		{ $$ = NULL; }
-	|	T_IMPLEMENTS class_name_list	{ $$ = $2; }
+	|	T_IMPLEMENTS class_name_with_generics_list	{ $$ = $2; }
 ;
 
 foreach_variable:
@@ -976,8 +996,13 @@ class_statement:
 ;
 
 class_name_list:
+		simple_class_name { $$ = zend_ast_create_list(1, ZEND_AST_NAME_LIST, $1); }
+	|	class_name_list ',' simple_class_name { $$ = zend_ast_list_add($1, $3); }
+;
+
+class_name_with_generics_list:
 		class_name { $$ = zend_ast_create_list(1, ZEND_AST_NAME_LIST, $1); }
-	|	class_name_list ',' class_name { $$ = zend_ast_list_add($1, $3); }
+	|	class_name_with_generics_list ',' class_name { $$ = zend_ast_list_add($1, $3); }
 ;
 
 trait_adaptations:
@@ -1029,7 +1054,7 @@ trait_method_reference:
 ;
 
 absolute_trait_method_reference:
-	class_name T_PAAMAYIM_NEKUDOTAYIM identifier
+	simple_class_name T_PAAMAYIM_NEKUDOTAYIM identifier
 		{ $$ = zend_ast_create(ZEND_AST_METHOD_REFERENCE, $1, $3); }
 ;
 
@@ -1408,7 +1433,7 @@ function_call:
 			if (zend_lex_tstring(&zv, $1) == FAILURE) { YYABORT; }
 			$$ = zend_ast_create(ZEND_AST_CALL, zend_ast_create_zval(&zv), $2);
 		}
-	|	class_name T_PAAMAYIM_NEKUDOTAYIM member_name argument_list
+	|	simple_class_name T_PAAMAYIM_NEKUDOTAYIM member_name argument_list
 			{ $$ = zend_ast_create(ZEND_AST_STATIC_CALL, $1, $3, $4); }
 	|	variable_class_name T_PAAMAYIM_NEKUDOTAYIM member_name argument_list
 			{ $$ = zend_ast_create(ZEND_AST_STATIC_CALL, $1, $3, $4); }
@@ -1418,17 +1443,31 @@ function_call:
 		}
 ;
 
-class_name:
+simple_class_name:
 		T_STATIC
 			{ zval zv; ZVAL_INTERNED_STR(&zv, ZSTR_KNOWN(ZEND_STR_STATIC));
 			  $$ = zend_ast_create_zval_ex(&zv, ZEND_NAME_NOT_FQ); }
 	|	name { $$ = $1; }
 ;
 
+class_name:
+		simple_class_name
+			{ $$ = zend_ast_create(ZEND_AST_CLASS_REF, $1, NULL); }
+	|	simple_class_name '<' generic_arg_list '>'
+			{ $$ = zend_ast_create(ZEND_AST_CLASS_REF, $1, $3); }
+;
+
+generic_arg_list:
+		type_expr
+			{ $$ = zend_ast_create_list(1, ZEND_AST_GENERIC_ARG_LIST, $1); }
+	|	generic_arg_list ',' type_expr
+			{ $$ = zend_ast_list_add($1, $3); }
+;
+
 class_name_reference:
-		class_name		{ $$ = $1; }
-	|	new_variable	{ $$ = $1; }
-	|	'(' expr ')'	{ $$ = $2; }
+		simple_class_name	{ $$ = $1; }
+	|	new_variable	    { $$ = $1; }
+	|	'(' expr ')'	    { $$ = $2; }
 ;
 
 backticks_expr:
@@ -1478,11 +1517,11 @@ constant:
 ;
 
 class_constant:
-		class_name T_PAAMAYIM_NEKUDOTAYIM identifier
+		simple_class_name T_PAAMAYIM_NEKUDOTAYIM identifier
 			{ $$ = zend_ast_create_class_const_or_name($1, $3); }
 	|	variable_class_name T_PAAMAYIM_NEKUDOTAYIM identifier
 			{ $$ = zend_ast_create_class_const_or_name($1, $3); }
-	|	class_name T_PAAMAYIM_NEKUDOTAYIM '{' expr '}'
+	|	simple_class_name T_PAAMAYIM_NEKUDOTAYIM '{' expr '}'
 			{ $$ = zend_ast_create(ZEND_AST_CLASS_CONST, $1, $4); }
 	|	variable_class_name T_PAAMAYIM_NEKUDOTAYIM '{' expr '}'
 			{ $$ = zend_ast_create(ZEND_AST_CLASS_CONST, $1, $4); }
@@ -1550,7 +1589,7 @@ simple_variable:
 ;
 
 static_member:
-		class_name T_PAAMAYIM_NEKUDOTAYIM simple_variable
+		simple_class_name T_PAAMAYIM_NEKUDOTAYIM simple_variable
 			{ $$ = zend_ast_create(ZEND_AST_STATIC_PROP, $1, $3); }
 	|	variable_class_name T_PAAMAYIM_NEKUDOTAYIM simple_variable
 			{ $$ = zend_ast_create(ZEND_AST_STATIC_PROP, $1, $3); }
@@ -1565,7 +1604,7 @@ new_variable:
 			{ $$ = zend_ast_create(ZEND_AST_PROP, $1, $3); }
 	|	new_variable T_NULLSAFE_OBJECT_OPERATOR property_name
 			{ $$ = zend_ast_create(ZEND_AST_NULLSAFE_PROP, $1, $3); }
-	|	class_name T_PAAMAYIM_NEKUDOTAYIM simple_variable
+	|	simple_class_name T_PAAMAYIM_NEKUDOTAYIM simple_variable
 			{ $$ = zend_ast_create(ZEND_AST_STATIC_PROP, $1, $3); }
 	|	new_variable T_PAAMAYIM_NEKUDOTAYIM simple_variable
 			{ $$ = zend_ast_create(ZEND_AST_STATIC_PROP, $1, $3); }
diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c
index 6e7d31e15a40f..990cf6c36dfbd 100644
--- a/Zend/zend_opcode.c
+++ b/Zend/zend_opcode.c
@@ -118,7 +118,7 @@ ZEND_API void zend_type_release(zend_type type, bool persistent) {
 		if (!ZEND_TYPE_USES_ARENA(type)) {
 			pefree(ZEND_TYPE_LIST(type), persistent);
 		}
-	} else if (ZEND_TYPE_HAS_NAME(type)) {
+	} else if (ZEND_TYPE_HAS_NAME(type) || ZEND_TYPE_IS_GENERIC_PARAM_NAME(type)) {
 		zend_string_release(ZEND_TYPE_NAME(type));
 	}
 }
@@ -333,6 +333,20 @@ ZEND_API void destroy_zend_class(zval *zv)
 		return;
 	}
 
+	bool persistent = ce->type == ZEND_INTERNAL_CLASS;
+	/* Common to internal and user classes */
+	if (ce->bound_types) {
+		zend_hash_release(ce->bound_types);
+	}
+	if (ce->num_generic_parameters > 0) {
+		for (uint32_t generic_param_index = 0; generic_param_index < ce->num_generic_parameters; generic_param_index++) {
+			const zend_generic_parameter generic_param = ce->generic_parameters[generic_param_index];
+			zend_string_release(generic_param.name);
+			zend_type_release(generic_param.constraint, persistent);
+		}
+		pefree(ce->generic_parameters, persistent);
+	}
+
 	switch (ce->type) {
 		case ZEND_USER_CLASS:
 			if (!(ce->ce_flags & ZEND_ACC_CACHED)) {
diff --git a/Zend/zend_types.h b/Zend/zend_types.h
index 4a6d00b9d73ea..f88cb28e4dac2 100644
--- a/Zend/zend_types.h
+++ b/Zend/zend_types.h
@@ -133,7 +133,7 @@ typedef struct {
 	 * are only supported since C++20). */
 	void *ptr;
 	uint32_t type_mask;
-	/* TODO: We could use the extra 32-bit of padding on 64-bit systems. */
+	uint32_t generic_param_index;
 } zend_type;
 
 typedef struct {
@@ -141,14 +141,20 @@ typedef struct {
 	zend_type types[1];
 } zend_type_list;
 
-#define _ZEND_TYPE_EXTRA_FLAGS_SHIFT 25
-#define _ZEND_TYPE_MASK ((1u << 25) - 1)
+typedef struct {
+	zend_string *name;
+	zend_type constraint;
+} zend_generic_parameter;
+
+#define _ZEND_TYPE_EXTRA_FLAGS_SHIFT 26
+#define _ZEND_TYPE_MASK ((1u << 26) - 1)
 /* Only one of these bits may be set. */
+#define _ZEND_TYPE_GENERIC_PARAM_NAME_BIT (1u << 25)
 #define _ZEND_TYPE_NAME_BIT (1u << 24)
 // Used to signify that type.ptr is not a `zend_string*` but a `const char*`,
 #define _ZEND_TYPE_LITERAL_NAME_BIT (1u << 23)
 #define _ZEND_TYPE_LIST_BIT (1u << 22)
-#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT|_ZEND_TYPE_LITERAL_NAME_BIT)
+#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT|_ZEND_TYPE_LITERAL_NAME_BIT|_ZEND_TYPE_GENERIC_PARAM_NAME_BIT)
 /* For BC behaviour with iterable type */
 #define _ZEND_TYPE_ITERABLE_BIT (1u << 21)
 /* Whether the type list is arena allocated */
@@ -166,7 +172,7 @@ typedef struct {
 	(((t).type_mask & _ZEND_TYPE_MASK) != 0)
 
 /* If a type is complex it means it's either a list with a union or intersection,
- * or the void pointer is a class name */
+ * the void pointer is a class name, or the type is a generic parameter name */
 #define ZEND_TYPE_IS_COMPLEX(t) \
 	((((t).type_mask) & _ZEND_TYPE_KIND_MASK) != 0)
 
@@ -179,6 +185,9 @@ typedef struct {
 #define ZEND_TYPE_HAS_LIST(t) \
 	((((t).type_mask) & _ZEND_TYPE_LIST_BIT) != 0)
 
+#define ZEND_TYPE_IS_GENERIC_PARAM_NAME(t) \
+	((((t).type_mask) & _ZEND_TYPE_GENERIC_PARAM_NAME_BIT) != 0)
+
 #define ZEND_TYPE_IS_ITERABLE_FALLBACK(t) \
 	((((t).type_mask) & _ZEND_TYPE_ITERABLE_BIT) != 0)
 
@@ -298,10 +307,10 @@ typedef struct {
 #endif
 
 #define ZEND_TYPE_INIT_NONE(extra_flags) \
-	_ZEND_TYPE_PREFIX { NULL, (extra_flags) }
+	_ZEND_TYPE_PREFIX { NULL, (extra_flags), 0 }
 
 #define ZEND_TYPE_INIT_MASK(_type_mask) \
-	_ZEND_TYPE_PREFIX { NULL, (_type_mask) }
+	_ZEND_TYPE_PREFIX { NULL, (_type_mask), 0 }
 
 #define ZEND_TYPE_INIT_CODE(code, allow_null, extra_flags) \
 	ZEND_TYPE_INIT_MASK(((code) == _IS_BOOL ? MAY_BE_BOOL : ( (code) == IS_ITERABLE ? _ZEND_TYPE_ITERABLE_BIT : ((code) == IS_MIXED ? MAY_BE_ANY : (1 << (code))))) \
@@ -309,16 +318,17 @@ typedef struct {
 
 #define ZEND_TYPE_INIT_PTR(ptr, type_kind, allow_null, extra_flags) \
 	_ZEND_TYPE_PREFIX { (void *) (ptr), \
-		(type_kind) | ((allow_null) ? _ZEND_TYPE_NULLABLE_BIT : 0) | (extra_flags) }
+		(type_kind) | ((allow_null) ? _ZEND_TYPE_NULLABLE_BIT : 0) | (extra_flags), \
+		0 }
 
 #define ZEND_TYPE_INIT_PTR_MASK(ptr, type_mask) \
-	_ZEND_TYPE_PREFIX { (void *) (ptr), (type_mask) }
+	_ZEND_TYPE_PREFIX { (void *) (ptr), (type_mask), 0 }
 
 #define ZEND_TYPE_INIT_UNION(ptr, extra_flags) \
-	_ZEND_TYPE_PREFIX { (void *) (ptr), (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_UNION_BIT) | (extra_flags) }
+	_ZEND_TYPE_PREFIX { (void *) (ptr), (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_UNION_BIT) | (extra_flags), 0 }
 
 #define ZEND_TYPE_INIT_INTERSECTION(ptr, extra_flags) \
-	_ZEND_TYPE_PREFIX { (void *) (ptr), (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_INTERSECTION_BIT) | (extra_flags) }
+	_ZEND_TYPE_PREFIX { (void *) (ptr), (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_INTERSECTION_BIT) | (extra_flags), 0 }
 
 #define ZEND_TYPE_INIT_CLASS(class_name, allow_null, extra_flags) \
 	ZEND_TYPE_INIT_PTR(class_name, _ZEND_TYPE_NAME_BIT, allow_null, extra_flags)
@@ -332,6 +342,9 @@ typedef struct {
 #define ZEND_TYPE_INIT_CLASS_CONST_MASK(class_name, type_mask) \
 	ZEND_TYPE_INIT_PTR_MASK(class_name, (_ZEND_TYPE_LITERAL_NAME_BIT | (type_mask)))
 
+#define ZEND_TYPE_INIT_GENERIC_PARAM(generic_name, index) \
+	_ZEND_TYPE_PREFIX { (void *) (generic_name), _ZEND_TYPE_GENERIC_PARAM_NAME_BIT, index }
+
 typedef union _zend_value {
 	zend_long         lval;				/* long value */
 	double            dval;				/* double value */
diff --git a/ext/zend_test/tests/compile_to_ast/interface_with_generic_types.phpt b/ext/zend_test/tests/compile_to_ast/interface_with_generic_types.phpt
new file mode 100644
index 0000000000000..d57bce88a1f70
--- /dev/null
+++ b/ext/zend_test/tests/compile_to_ast/interface_with_generic_types.phpt
@@ -0,0 +1,58 @@
+--TEST--
+AST can be recreated (interface with generic types)
+--EXTENSIONS--
+zend_test
+--FILE--
+<?php
+
+namespace {
+    interface MyInterface1<T1 : string|Stringable|int, T2> {
+        public function bar(T1 $v): T2;
+    }
+}
+
+namespace Foo {
+    interface MyInterface2<S : string|\Stringable|int> extends \MyInterface1<S, S> {
+        public function foobar(S $v): int;
+    }
+
+    class MyClass implements MyInterface2<string> {
+        public function bar(string $v): string {}
+        public function foobar(string $v): int {}
+    }
+}
+
+namespace {
+    echo zend_test_compile_to_ast( file_get_contents( __FILE__ ) );
+}
+
+?>
+--EXPECT--
+namespace {
+    interface MyInterface1<T1 : string|Stringable|int, T2> {
+        public function bar(T1 $v): T2;
+
+    }
+
+}
+
+namespace Foo {
+    interface MyInterface2<S : string|\Stringable|int> implements \MyInterface1<S, S> {
+        public function foobar(S $v): int;
+
+    }
+
+    class MyClass implements MyInterface2<string> {
+        public function bar(string $v): string {
+        }
+
+        public function foobar(string $v): int {
+        }
+
+    }
+
+}
+
+namespace {
+    echo zend_test_compile_to_ast(file_get_contents(__FILE__));
+}