Skip to content

Commit 634827a

Browse files
php-genericsclaude
andcommitted
Add progressive generic inference, generic args interning, and inline cache
Progressive inference: when a generic class is instantiated without type args (e.g., `new Collection()`), the object enters progressive mode that tracks min-type (widened from never) and max-type (narrowed from mixed). Values widen the lower bound; passing to typed functions narrows the upper bound. When min equals max, the object auto-freezes to regular generic_args. Generic args interning: deduplicate identical generic_args via a global HashTable keyed by type content hash, reducing memory for monomorphic usage patterns. Inline cache for generic class type hints: cache CE lookup and last-seen generic_args in run_time_cache slots, turning O(n*m) compatibility checks into O(1) for the common monomorphic case. Also fixes subtype compatibility so Box<int> is accepted where Box<int|string> is expected (subset check instead of exact equality). 92/92 generics tests, 5356/5356 Zend tests, 891/891 opcache tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 72009da commit 634827a

18 files changed

+1811
-22
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
--TEST--
2+
Generic class: generic args interning deduplicates identical type arguments
3+
--FILE--
4+
<?php
5+
declare(strict_types=1);
6+
7+
class Box<T> {
8+
public T $value;
9+
public function __construct(T $value) { $this->value = $value; }
10+
public function get(): T { return $this->value; }
11+
}
12+
13+
// Many instances with the same type args should work correctly (interning deduplicates)
14+
$boxes = [];
15+
for ($i = 0; $i < 100; $i++) {
16+
$boxes[] = new Box<int>($i);
17+
}
18+
echo "Created 100 Box<int>\n";
19+
20+
// Verify all instances preserve correct values
21+
for ($i = 0; $i < 100; $i++) {
22+
if ($boxes[$i]->get() !== $i) {
23+
echo "FAIL: Box[$i] has wrong value\n";
24+
exit;
25+
}
26+
}
27+
echo "All values correct\n";
28+
29+
// Inferred args should also be interned
30+
$inferred = [];
31+
for ($i = 0; $i < 50; $i++) {
32+
$inferred[] = new Box($i);
33+
}
34+
for ($i = 0; $i < 50; $i++) {
35+
if ($inferred[$i]->get() !== $i) {
36+
echo "FAIL: inferred Box[$i] has wrong value\n";
37+
exit;
38+
}
39+
}
40+
echo "Inferred args correct\n";
41+
42+
// Different type args must remain distinct
43+
$intBox = new Box<int>(42);
44+
$strBox = new Box<string>("hello");
45+
$floatBox = new Box<float>(3.14);
46+
47+
echo $intBox->get() . "\n";
48+
echo $strBox->get() . "\n";
49+
echo $floatBox->get() . "\n";
50+
51+
// Type enforcement still works after interning
52+
try {
53+
$intBox->value = "wrong";
54+
} catch (TypeError $e) {
55+
echo "int box: caught\n";
56+
}
57+
58+
try {
59+
$strBox->value = 123;
60+
} catch (TypeError $e) {
61+
echo "str box: caught\n";
62+
}
63+
64+
// Multi-param generic args interning
65+
class Pair<T, U> {
66+
public function __construct(public T $first, public U $second) {}
67+
}
68+
69+
$pairs = [];
70+
for ($i = 0; $i < 50; $i++) {
71+
$pairs[] = new Pair<int, string>($i, "v$i");
72+
}
73+
echo "Created 50 Pair<int, string>\n";
74+
75+
for ($i = 0; $i < 50; $i++) {
76+
if ($pairs[$i]->first !== $i || $pairs[$i]->second !== "v$i") {
77+
echo "FAIL: Pair[$i] has wrong values\n";
78+
exit;
79+
}
80+
}
81+
echo "All pairs correct\n";
82+
83+
// Expanded defaults interning: DefaultMap<int> expands to DefaultMap<int, string>
84+
class DefaultMap<T, U = string> {
85+
public function __construct(public T $key, public U $val) {}
86+
}
87+
88+
// Explicit 2 args — no expansion
89+
$m1 = new DefaultMap<int, string>(1, "a");
90+
$m2 = new DefaultMap<int, string>(2, "b");
91+
echo $m1->key . ":" . $m1->val . "\n";
92+
echo $m2->key . ":" . $m2->val . "\n";
93+
94+
// Only 1 arg — V defaults to string, triggers expansion + interning
95+
$m3 = new DefaultMap<int>(3, "c");
96+
$m4 = new DefaultMap<int>(4, "d");
97+
echo $m3->key . ":" . $m3->val . "\n";
98+
echo $m4->key . ":" . $m4->val . "\n";
99+
100+
// Generic function inference produces interned args
101+
function identity<T>(T $v): T { return $v; }
102+
echo identity(42) . "\n";
103+
echo identity("hello") . "\n";
104+
105+
// Context resolution interning: new Box<T> inside a generic factory
106+
class Factory<T> {
107+
public static function make(T $val): Box<T> {
108+
return new Box<T>($val);
109+
}
110+
}
111+
112+
$fb1 = Factory<int>::make(100);
113+
$fb2 = Factory<int>::make(200);
114+
echo $fb1->get() . "\n";
115+
echo $fb2->get() . "\n";
116+
117+
echo "OK\n";
118+
?>
119+
--EXPECT--
120+
Created 100 Box<int>
121+
All values correct
122+
Inferred args correct
123+
42
124+
hello
125+
3.14
126+
int box: caught
127+
str box: caught
128+
Created 50 Pair<int, string>
129+
All pairs correct
130+
1:a
131+
2:b
132+
3:c
133+
4:d
134+
42
135+
hello
136+
100
137+
200
138+
OK
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
--TEST--
2+
Generic class: inline cache for generic class type hints in function signatures
3+
--FILE--
4+
<?php
5+
declare(strict_types=1);
6+
7+
class Box<T> {
8+
public T $value;
9+
public function __construct(T $value) { $this->value = $value; }
10+
public function get(): T { return $this->value; }
11+
}
12+
13+
// Function with generic class type hint — exercises inline CE + args cache
14+
function acceptBoxInt(Box<int> $box): int {
15+
return $box->get();
16+
}
17+
18+
function acceptBoxString(Box<string> $box): string {
19+
return $box->get();
20+
}
21+
22+
// First call populates the cache
23+
$b1 = new Box<int>(10);
24+
echo acceptBoxInt($b1) . "\n";
25+
26+
// Subsequent calls should hit the monomorphic cache
27+
$b2 = new Box<int>(20);
28+
$b3 = new Box<int>(30);
29+
echo acceptBoxInt($b2) . "\n";
30+
echo acceptBoxInt($b3) . "\n";
31+
32+
// Inferred args should also match cache (interned to same pointer)
33+
$b4 = new Box(40);
34+
echo acceptBoxInt($b4) . "\n";
35+
36+
// Different generic type — should not interfere
37+
$s1 = new Box<string>("hello");
38+
echo acceptBoxString($s1) . "\n";
39+
40+
// Wrong type should still be rejected
41+
try {
42+
acceptBoxInt(new Box<string>("wrong"));
43+
} catch (TypeError $e) {
44+
echo "rejected wrong type\n";
45+
}
46+
47+
// Return type with generic class
48+
function makeBoxInt(int $v): Box<int> {
49+
return new Box<int>($v);
50+
}
51+
52+
$r = makeBoxInt(99);
53+
echo $r->get() . "\n";
54+
55+
// Multiple calls to exercise return type cache
56+
echo makeBoxInt(100)->get() . "\n";
57+
echo makeBoxInt(101)->get() . "\n";
58+
59+
// Nested generic: Box<Box<int>> in function signature
60+
function acceptNestedBox(Box<Box<int>> $outer): int {
61+
return $outer->get()->get();
62+
}
63+
64+
$inner = new Box<int>(42);
65+
$outer = new Box<Box<int>>($inner);
66+
echo acceptNestedBox($outer) . "\n";
67+
68+
// Multiple calls to nested — exercises cache with complex types
69+
$inner2 = new Box<int>(99);
70+
$outer2 = new Box<Box<int>>($inner2);
71+
echo acceptNestedBox($outer2) . "\n";
72+
73+
// Generic param resolution does NOT use wrong cache (regression test for
74+
// the cross-op_array cache_slot bug with strict_types + nested generics)
75+
class Container<T> {
76+
public T $item;
77+
public function __construct(T $item) { $this->item = $item; }
78+
public function getItem(): T { return $this->item; }
79+
}
80+
81+
$boxed = new Box<int>(7);
82+
$cont = new Container<Box<int>>($boxed);
83+
echo $cont->getItem()->get() . "\n";
84+
85+
// Verify property type enforcement with resolved generic params
86+
try {
87+
$cont->item = "not a box";
88+
} catch (TypeError $e) {
89+
echo "container: caught\n";
90+
}
91+
92+
echo "OK\n";
93+
?>
94+
--EXPECT--
95+
10
96+
20
97+
30
98+
40
99+
hello
100+
rejected wrong type
101+
99
102+
100
103+
101
104+
42
105+
99
106+
7
107+
container: caught
108+
OK
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
--TEST--
2+
Generic class: no progressive inference — types are fixed at construction
3+
--DESCRIPTION--
4+
Verifies that generic type parameters are determined at construction time and
5+
remain fixed. There is no progressive/flow-sensitive inference from method calls.
6+
7+
Three scenarios:
8+
1. Explicit type args: type is fixed, subsequent calls are checked
9+
2. Constructor inference: type is inferred from ctor args, then fixed
10+
3. No inference possible: type is unconstrained (acts as mixed)
11+
12+
Also tests the variance scenario: Box(new Child1) produces Box<Child1>,
13+
which correctly rejects Child2 assignment and Box<Elder> parameter hints.
14+
--FILE--
15+
<?php
16+
declare(strict_types=1);
17+
18+
class Collection<T> {
19+
private array $items = [];
20+
public function add(T $item): void { $this->items[] = $item; }
21+
public function getAll(): array { return $this->items; }
22+
}
23+
24+
class Box<T> {
25+
public T $value;
26+
public function __construct(T $value) { $this->value = $value; }
27+
public function set(T $value): void { $this->value = $value; }
28+
public function get(): T { return $this->value; }
29+
}
30+
31+
// === 1. Explicit type args: fixed at construction ===
32+
echo "--- Explicit type args ---\n";
33+
$c = new Collection<int>();
34+
$c->add(1);
35+
$c->add(2);
36+
echo "Added ints: OK\n";
37+
38+
try {
39+
$c->add("text");
40+
} catch (TypeError $e) {
41+
echo "Rejected string in Collection<int>: OK\n";
42+
}
43+
44+
// === 2. Constructor inference: fixed after inference ===
45+
echo "--- Constructor inference ---\n";
46+
$box = new Box(42); // infers T=int
47+
echo "Inferred Box<int>: " . $box->get() . "\n";
48+
49+
try {
50+
$box->set("hello");
51+
} catch (TypeError $e) {
52+
echo "Rejected string in inferred Box<int>: OK\n";
53+
}
54+
55+
$sbox = new Box("hello"); // infers T=string
56+
echo "Inferred Box<string>: " . $sbox->get() . "\n";
57+
58+
try {
59+
$sbox->set([1, 2, 3]);
60+
} catch (TypeError $e) {
61+
echo "Rejected array in inferred Box<string>: OK\n";
62+
}
63+
64+
// === 3. No inference possible: unconstrained (acts as mixed) ===
65+
echo "--- No inference possible ---\n";
66+
$uc = new Collection();
67+
$uc->add(1);
68+
$uc->add("text");
69+
$uc->add(3.14);
70+
$uc->add(true);
71+
$uc->add(null);
72+
$uc->add([1, 2]);
73+
echo "Unconstrained accepts all types: OK\n";
74+
echo "Count: " . count($uc->getAll()) . "\n";
75+
76+
// === 4. Variance: inferred type prevents subtype mixing ===
77+
echo "--- Variance with inheritance ---\n";
78+
79+
class Elder {}
80+
class Child1 extends Elder {}
81+
class Child2 extends Elder {}
82+
83+
$b = new Box(new Child1()); // infers T=Child1
84+
85+
// Child2 is not Child1, should be rejected
86+
try {
87+
$b->value = new Child2();
88+
} catch (TypeError $e) {
89+
echo "Rejected Child2 in Box<Child1>: OK\n";
90+
}
91+
92+
// Another Child1 should be accepted
93+
$b->value = new Child1();
94+
echo "Accepted Child1 in Box<Child1>: OK\n";
95+
96+
// === 5. Explicit Box<Elder> accepts both children ===
97+
echo "--- Explicit wider type ---\n";
98+
$eb = new Box<Elder>(new Child1());
99+
$eb->value = new Child2();
100+
echo "Box<Elder> accepts Child2: OK\n";
101+
$eb->value = new Child1();
102+
echo "Box<Elder> accepts Child1: OK\n";
103+
104+
// === 6. Box<Child1> does not satisfy Box<Elder> (invariant) ===
105+
echo "--- Invariant type parameter ---\n";
106+
107+
function takesElderBox(Box<Elder> $b): string {
108+
return get_class($b->value);
109+
}
110+
111+
try {
112+
takesElderBox(new Box(new Child1()));
113+
} catch (TypeError $e) {
114+
echo "Box<Child1> rejected for Box<Elder> param: OK\n";
115+
}
116+
117+
// But Box<Elder> works
118+
echo "Box<Elder> accepted: " . takesElderBox(new Box<Elder>(new Child1())) . "\n";
119+
120+
echo "Done.\n";
121+
?>
122+
--EXPECT--
123+
--- Explicit type args ---
124+
Added ints: OK
125+
Rejected string in Collection<int>: OK
126+
--- Constructor inference ---
127+
Inferred Box<int>: 42
128+
Rejected string in inferred Box<int>: OK
129+
Inferred Box<string>: hello
130+
Rejected array in inferred Box<string>: OK
131+
--- No inference possible ---
132+
Unconstrained accepts all types: OK
133+
Count: 6
134+
--- Variance with inheritance ---
135+
Rejected Child2 in Box<Child1>: OK
136+
Accepted Child1 in Box<Child1>: OK
137+
--- Explicit wider type ---
138+
Box<Elder> accepts Child2: OK
139+
Box<Elder> accepts Child1: OK
140+
--- Invariant type parameter ---
141+
Box<Child1> rejected for Box<Elder> param: OK
142+
Box<Elder> accepted: Child1
143+
Done.

0 commit comments

Comments
 (0)