Skip to content

Commit 0db2304

Browse files
committed
fix inheritance chaining
Signed-off-by: Robert Landers <landers.robert@gmail.com> fix inheritance chaining Signed-off-by: Robert Landers <landers.robert@gmail.com> fix inheritance chaining Signed-off-by: Robert Landers <landers.robert@gmail.com>
1 parent 5ad9c0d commit 0db2304

15 files changed

Lines changed: 846 additions & 56 deletions
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
--TEST--
2+
Errors: a forwarded TYPE_PARAMETER ref in a `new C::<T>()` turbofish errors at the synth site when nothing pins T and its bound is not a class
3+
--FILE--
4+
<?php
5+
// Companion to type_param_in_new_expression.phpt (`new T()`). Same shape, but
6+
// the unresolvable T sits inside a turbofish on `new C::<T>(...)` instead of
7+
// being the class itself. Both must produce the same diagnostic, fired at the
8+
// `new` site — not silently produce a broken-refs monomorph that crashes far
9+
// from the cause when a method is later called on it.
10+
final readonly class Box<U = mixed> {
11+
public function __construct(public array $items = []) {}
12+
}
13+
14+
function makeBox<T>(): Box {
15+
return new Box::<T>([]);
16+
}
17+
18+
try {
19+
makeBox();
20+
echo "no error??\n";
21+
} catch (Error $e) {
22+
echo "ok: " . $e->getMessage() . "\n";
23+
}
24+
25+
// A class-bound on the outer T gives a fallback target.
26+
class Base {}
27+
function makeBoxFromBound<T : Base>(): Box {
28+
return new Box::<T>([]);
29+
}
30+
$b = makeBoxFromBound();
31+
var_dump($b::class);
32+
33+
// Turbofish supplied: no error, T resolves to the supplied type.
34+
$b = makeBox::<int>();
35+
var_dump($b::class);
36+
?>
37+
--EXPECT--
38+
ok: Cannot resolve generic type parameter T at runtime: no binding was supplied and its bound is not a class
39+
string(9) "Box<Base>"
40+
string(8) "Box<int>"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
--TEST--
2+
Parametric LSP: child implementing `I<TN>` with its own TN forwarded to parent's TN
3+
--FILE--
4+
<?php
5+
// Regression: when both parent and child bind the same parameter shape
6+
// (parent's TN, child's TN, both bare type-parameter refs), the
7+
// inheritance check needs to treat them as equivalent. The parent's
8+
// substituted return is itself a type-parameter ref so the substitute
9+
// helper falls back to the erased form; the child must follow the same
10+
// fall-back, or the check sees `mixed` vs `TN` and rejects what's
11+
// structurally an identity binding.
12+
13+
interface GI<TN = mixed, TW = mixed> {
14+
public function getEdgesFrom(TN $from): array;
15+
}
16+
17+
class DG<TN = mixed, TW = mixed> implements GI<TN, TW> {
18+
public function getEdgesFrom(TN $from): array { return []; }
19+
}
20+
21+
echo "OK\n";
22+
?>
23+
--EXPECT--
24+
OK
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
--TEST--
2+
Parametric LSP: method-level type-parameter bound that references a class-scope T substitutes when the subclass binds T
3+
--FILE--
4+
<?php
5+
// Regression: `class A<T> { function set<U : T>(U $x) }` — the method's
6+
// own U has a bound that's a class-scope T-ref. When `class B extends A<string>`
7+
// supplies T = string, the inheritance check on the inherited (or overridden)
8+
// method must see U's bound substituted from T → string, otherwise the parent
9+
// renders as `set(mixed $x)` (T erased) and the child's `set<U:string>(U $x)`
10+
// gets rejected as narrower than mixed.
11+
12+
class A<T> {
13+
public function set<U : T>(U $x): void {}
14+
}
15+
16+
class B extends A<string> {
17+
public function set<U : string>(U $x): void {}
18+
}
19+
20+
class C<Tl, Tr> {
21+
public function set<U : Tl|Tr>(U $x): void {}
22+
}
23+
24+
class D extends C<string, int> {
25+
public function set<U : string|int>(U $x): void {}
26+
}
27+
28+
echo "OK\n";
29+
?>
30+
--EXPECT--
31+
OK
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
--TEST--
2+
Parametric LSP: child narrowing Tl|Tr return down to a single Tl is accepted
3+
--FILE--
4+
<?php
5+
// Companion to union_of_type_params_return_subst.phpt: a child that
6+
// returns a narrower subset of the parent's substituted Tl|Tr — covariant.
7+
8+
abstract class Base<T> {
9+
abstract public function get(): T;
10+
}
11+
12+
class LeftOnly<Tl, Tr> extends Base<Tl|Tr> {
13+
public function __construct(private Tl $value) {}
14+
public function get(): Tl { return $this->value; }
15+
}
16+
17+
$a = new LeftOnly::<string, int>("hi");
18+
var_dump($a->get());
19+
20+
$b = new LeftOnly::<int, string>(42);
21+
var_dump($b->get());
22+
?>
23+
--EXPECT--
24+
string(2) "hi"
25+
int(42)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
--TEST--
2+
Parametric LSP: child returning union of two type parameters (Tl|Tr) satisfies parent's T when parent is bound to Tl|Tr
3+
--FILE--
4+
<?php
5+
// Regression for: child override declaring `: Tl|Tr` was reported as
6+
// `: mixed` against the parent's substituted `: T = Tl|Tr`, even though
7+
// the two are structurally identical.
8+
//
9+
// Triggered originally by Psl\Type\Internal\UnionType extending
10+
// Psl\Type\Type<Tl|Tr> with overrides returning Tl|Tr.
11+
12+
abstract class Base<T> {
13+
abstract public function get(): T;
14+
}
15+
16+
class Pair<Tl, Tr> extends Base<Tl|Tr> {
17+
public function __construct(private Tl|Tr $value) {}
18+
public function get(): Tl|Tr { return $this->value; }
19+
}
20+
21+
$p = new Pair::<string, int>("hi");
22+
var_dump($p->get());
23+
24+
$q = new Pair::<string, int>(42);
25+
var_dump($q->get());
26+
27+
// Also exercise the interface-implements path, which is what the Psl
28+
// failure actually hit (UnionType extends Type which implements TypeInterface).
29+
interface I<+T> {
30+
public function read(mixed $v): T;
31+
}
32+
33+
abstract class IBase<T> implements I<T> {
34+
abstract public function read(mixed $v): T;
35+
}
36+
37+
class Union<Tl, Tr> extends IBase<Tl|Tr> {
38+
public function read(mixed $v): Tl|Tr {
39+
if ($v instanceof Tl || $v instanceof Tr) return $v;
40+
throw new TypeError("nope");
41+
}
42+
}
43+
44+
echo "loaded\n";
45+
?>
46+
--EXPECT--
47+
string(2) "hi"
48+
int(42)
49+
loaded
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--TEST--
2+
Parametric LSP: child returning Tl|Tr|string (a widening of the parent's substituted Tl|Tr) is rejected
3+
--FILE--
4+
<?php
5+
// After the pre-erasure mask aggregation, the covariant check sees that
6+
// the child returns a strictly wider set than the parent.
7+
8+
abstract class Base<T> {
9+
abstract public function get(): T;
10+
}
11+
12+
class BadPair<Tl, Tr> extends Base<Tl|Tr> {
13+
public function get(): Tl|Tr|string { throw new Exception(); }
14+
}
15+
?>
16+
--EXPECTF--
17+
Fatal error: Declaration of BadPair::get(): Tl|Tr|string must be compatible with Base::get(): Tl|Tr in %s on line %d

Zend/tests/generics/reification/forwarded_t_ref_value_check.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ object(Dog)#%d (0) {
8181
caught: id(): Argument #1 ($x) must be of type Dog, Cat given
8282
int(42)
8383
caught: id(): Argument #1 ($x) must be of type int, string given
84-
caught: id(): Argument #1 ($x) must be of type int|string, array given
84+
caught: id(): Argument #1 ($x) must be of type string|int, array given
8585
object(Both)#%d (0) {
8686
}
8787
caught: id(): Argument #1 ($x) must be of type Fooable&Barable, FooOnly given
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--TEST--
2+
Reification: a monomorph of `B<T> implements I<T>` should report `is_a I<bound>` true
3+
--FILE--
4+
<?php
5+
// Regression: when `B<T> implements I<T>` is monomorphized as `B<string>`,
6+
// the implements binding has to be substituted so that `B<string>` is also
7+
// known to implement `I<string>`. Otherwise `instanceof I<string>` /
8+
// `is_a(..., 'I<string>')` returns false even though the type relationship
9+
// holds, and property writes typed `I<string>` reject `B<string>` values.
10+
11+
interface I<+T> {}
12+
class B<T> implements I<T> {}
13+
14+
// 1) Direct monomorph
15+
$b = new B::<string>();
16+
var_dump(is_a($b, 'I<string>'));
17+
var_dump(is_a($b, 'B<string>'));
18+
$mono = 'B<string>';
19+
var_dump($b instanceof $mono);
20+
21+
// 2) Non-generic subclass of a monomorph
22+
class StrB extends B<string> {}
23+
$s = new StrB();
24+
var_dump(is_a($s, 'I<string>'));
25+
var_dump(is_a($s, 'B<string>'));
26+
27+
// 3) Assignment to a property typed I<string> accepts a B<string>
28+
class Holder<T> {
29+
public I<T> $val;
30+
}
31+
$h = new Holder::<string>();
32+
$h->val = new B::<string>();
33+
var_dump($h->val::class);
34+
?>
35+
--EXPECT--
36+
bool(true)
37+
bool(true)
38+
bool(true)
39+
bool(true)
40+
bool(true)
41+
string(9) "B<string>"
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
--TEST--
2+
Reification: a property typed `I<T>` (T inside a generic class type arg) substitutes T → binding for the monomorph
3+
--FILE--
4+
<?php
5+
// Regression: `public I<T> $val` on a generic class. When the class is
6+
// monomorphized (e.g. `new Box::<string>()`), the property type should
7+
// substitute T → string and the runtime check should accept values that
8+
// satisfy `I<string>`. The bug was that `zend_substitute_leaf_type_param`
9+
// didn't recurse into NAMED_WITH_ARGS payloads, so the property stayed
10+
// typed as the unsubstituted `I<T>` and any assignment failed with
11+
// "Cannot assign ... of type I<T>".
12+
13+
interface I<+T> {}
14+
15+
class StrImpl implements I<string> {}
16+
class IntImpl implements I<int> {}
17+
18+
class Box<T> {
19+
public I<T> $val;
20+
}
21+
22+
// Direct monomorph use.
23+
$b = new Box::<string>();
24+
$b->val = new StrImpl();
25+
var_dump($b->val::class);
26+
27+
// Wrong-arg implementation rejected.
28+
try {
29+
$b->val = new IntImpl();
30+
} catch (TypeError $e) {
31+
echo "1: ", $e->getMessage(), "\n";
32+
}
33+
34+
// Inherited subclass case: same substitution applies when the property is
35+
// touched on an instance of a non-generic child. Use `mixed` on the ctor
36+
// so the bad value reaches the property assignment site, exercising the
37+
// inherited property's substituted type rather than the ctor's own type.
38+
class StringBox extends Box<string> {
39+
public function __construct(mixed $val) {
40+
$this->val = $val;
41+
}
42+
}
43+
$sb = new StringBox(new StrImpl());
44+
var_dump($sb->val::class);
45+
46+
try {
47+
new StringBox(new IntImpl());
48+
} catch (TypeError $e) {
49+
echo "2: ", $e->getMessage(), "\n";
50+
}
51+
?>
52+
--EXPECTF--
53+
string(7) "StrImpl"
54+
1: Cannot assign IntImpl to property %s::$val of type I<string>
55+
string(7) "StrImpl"
56+
2: Cannot assign IntImpl to property %s::$val of type I<string>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
--TEST--
2+
Reification: `new SelfClass::<T>(...)` inside a generic class method resolves T against the instance's monomorph args at synth time
3+
--FILE--
4+
<?php
5+
// Companion to new_turbofish_with_outer_t_ref.phpt — same fix but the T-ref
6+
// origin is CLASS_LIKE: a generic class's method does `new Self::<T>(...)`
7+
// using its own class-level T's. Runtime synth must walk the called scope
8+
// up to the lexical class's monomorph descendant to resolve the binding.
9+
10+
final readonly class Bag<T = mixed> {
11+
public function __construct(public array $items = []) {}
12+
13+
public function withItem(T $item): Bag {
14+
$items = $this->items;
15+
$items[] = $item;
16+
// Forward T explicitly — must resolve against the current instance's
17+
// monomorph (Bag<int> stays Bag<int>, not Bag<T>).
18+
return new Bag::<T>($items);
19+
}
20+
}
21+
22+
$b = new Bag::<int>([1, 2, 3]);
23+
var_dump($b::class);
24+
$b = $b->withItem(4);
25+
var_dump($b::class);
26+
$b = $b->withItem(5);
27+
var_dump($b::class);
28+
29+
$s = new Bag::<string>(['a']);
30+
var_dump($s::class);
31+
$s = $s->withItem('b');
32+
var_dump($s::class);
33+
34+
// Calling withItem on a bare-default Bag<mixed> still works — forwarding
35+
// resolves T to its (default) mixed binding from the monomorph.
36+
$d = new Bag();
37+
var_dump($d::class);
38+
$d = $d->withItem(42);
39+
var_dump($d::class);
40+
?>
41+
--EXPECT--
42+
string(8) "Bag<int>"
43+
string(8) "Bag<int>"
44+
string(8) "Bag<int>"
45+
string(11) "Bag<string>"
46+
string(11) "Bag<string>"
47+
string(10) "Bag<mixed>"
48+
string(10) "Bag<mixed>"

0 commit comments

Comments
 (0)