Skip to content

Commit 9076fd1

Browse files
committed
make new static::<> a compile time error
Signed-off-by: Robert Landers <landers.robert@gmail.com>
1 parent 5e28858 commit 9076fd1

6 files changed

Lines changed: 149 additions & 0 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
--TEST--
2+
Type arguments on `new static::<...>` are a compile-time error (static is the runtime type)
3+
--FILE--
4+
<?php
5+
// `static` is the runtime type, which already carries its own type arguments
6+
// and whose parameter list is unknown at this lexical site (it may be a generic
7+
// subclass with a different list, or a non-generic subclass of a monomorph).
8+
// The written args name the *enclosing* class's parameters, so applying them to
9+
// `static` is a scoping error — rejected at compile time rather than crashing
10+
// or throwing ArgumentCountError at runtime.
11+
class C<TKey, T> {
12+
public function __construct(private array $e = []) {}
13+
public function dup(): static { return new static::<TKey, T>($this->e); }
14+
}
15+
(new C::<mixed, mixed>([1, 2]))->dup();
16+
?>
17+
--EXPECTF--
18+
Fatal error: Type arguments cannot be applied to "static": "static" is the runtime type and already carries its type arguments. Use "new static()" to construct the exact runtime type, or "new self::<...>" to apply type arguments to the enclosing class in %s on line %d
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--TEST--
2+
`new static::<...>` is rejected even when the runtime type is a non-generic subclass of a monomorph
3+
--FILE--
4+
<?php
5+
// Here `static` would resolve to IntBox, which extends the monomorph Box<int>
6+
// but is itself non-generic — it has no type parameters to apply `<T>` to. The
7+
// rejection is the same compile-time scoping error: use `new static()`.
8+
class Box<T> {
9+
public function __construct(private mixed $v = null) {}
10+
public function make(): object { return new static::<T>($this->v); }
11+
}
12+
class IntBox extends Box<int> {}
13+
14+
(new IntBox(5))->make();
15+
?>
16+
--EXPECTF--
17+
Fatal error: Type arguments cannot be applied to "static": "static" is the runtime type and already carries its type arguments. Use "new static()" to construct the exact runtime type, or "new self::<...>" to apply type arguments to the enclosing class in %s on line %d
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
Reification: T is inferred from scalar/array arguments, not just objects
3+
--FILE--
4+
<?php
5+
function tparams<T = mixed>(T $a): array { return func_get_type_args(); }
6+
7+
var_dump(tparams(1)["T"]);
8+
var_dump(tparams("foo")["T"]);
9+
var_dump(tparams(1.5)["T"]);
10+
var_dump(tparams(true)["T"]);
11+
var_dump(tparams(false)["T"]);
12+
var_dump(tparams([1, 2])["T"]);
13+
var_dump(tparams(new stdClass())["T"]);
14+
?>
15+
--EXPECT--
16+
string(3) "int"
17+
string(6) "string"
18+
string(5) "float"
19+
string(4) "bool"
20+
string(4) "bool"
21+
string(5) "array"
22+
string(8) "stdClass"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
--TEST--
2+
Reification: a :T return type reified from an inferred scalar T is enforced
3+
--FILE--
4+
<?php
5+
// T is inferred from the argument. A method declared `: T` must therefore
6+
// enforce the inferred scalar type on its return value rather than silently
7+
// erasing T to mixed.
8+
function bad<T = mixed>(T $a): T { return "not the same type"; }
9+
10+
echo "calling bad(1) — T inferred as int, returning a string must fail\n";
11+
try {
12+
var_dump(bad(1));
13+
} catch (TypeError $e) {
14+
echo "TypeError: ", $e->getMessage(), "\n";
15+
}
16+
17+
// Returning a matching type is fine.
18+
function good<T = mixed>(T $a): T { return $a; }
19+
var_dump(good(42));
20+
var_dump(good("ok"));
21+
?>
22+
--EXPECT--
23+
calling bad(1) — T inferred as int, returning a string must fail
24+
TypeError: bad(): Return value must be of type int, string returned
25+
int(42)
26+
string(2) "ok"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
Reification: `new static()` reproduces the exact runtime type; `new self::<...>` re-applies the lexical template
3+
--FILE--
4+
<?php
5+
// `new static()` (no turbofish) is the correct way to construct "another of
6+
// exactly me": the runtime type already carries its type arguments, so a
7+
// monomorph clones to the same monomorph and a subclass clones to itself.
8+
// `new self::<...>` remains the way to apply type arguments to the enclosing
9+
// (lexical) class — including re-binding to different args.
10+
class C<TKey, T> {
11+
public function __construct(private array $e = []) {}
12+
13+
public function dupStatic(): static { return new static($this->e); }
14+
public function dupSelf(): static { return new self::<TKey, T>($this->e); }
15+
public function swapSelf(): object { return new self::<T, TKey>($this->e); }
16+
public function reSelf(): object { return new self::<float, float>($this->e); }
17+
}
18+
19+
$o = new C::<int, string>([1, 2]);
20+
echo "orig: ", $o::class, "\n";
21+
echo "dupStatic: ", $o->dupStatic()::class, "\n";
22+
echo "dupSelf: ", $o->dupSelf()::class, "\n";
23+
echo "swapSelf: ", $o->swapSelf()::class, "\n";
24+
echo "reSelf: ", $o->reSelf()::class, "\n";
25+
26+
// `new static()` through a non-generic subclass of a monomorph reproduces the
27+
// subclass, exactly as plain LSB does.
28+
class Box<T> {
29+
public function __construct(public mixed $v = null) {}
30+
public function copy(): static { return new static($this->v); }
31+
}
32+
class IntBox extends Box<int> {}
33+
34+
$b = new IntBox(5);
35+
echo "IntBox: ", $b::class, "\n";
36+
echo "IntBox copy: ", $b->copy()::class, "\n";
37+
?>
38+
--EXPECT--
39+
orig: C<int,string>
40+
dupStatic: C<int,string>
41+
dupSelf: C<int,string>
42+
swapSelf: C<string,int>
43+
reSelf: C<float,float>
44+
IntBox: IntBox
45+
IntBox copy: IntBox

Zend/zend_compile.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8097,6 +8097,27 @@ static void zend_compile_new(znode *result, zend_ast *ast) /* {{{ */
80978097
}
80988098
}
80998099

8100+
/* `new static::<...>` is always rejected at compile time. The type
8101+
* arguments are written in the lexical scope — they name the enclosing
8102+
* class's (`self`'s) type parameters — but `static` is the *runtime* type,
8103+
* whose parameter list is unknown here and need not match: it may be a
8104+
* generic subclass with a different parameter list, or a non-generic
8105+
* subclass of a monomorph (e.g. `class IntBox extends Box<int>`) with no
8106+
* type parameters at all. `static` already carries its own bindings, so
8107+
* `new static()` reproduces the exact runtime type; `new self::<...>`
8108+
* re-applies the lexical template. */
8109+
if (opline->op1_type == IS_UNUSED && turbofish_ast
8110+
&& (opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_STATIC) {
8111+
zend_ast_list *tf_list = zend_ast_get_list(turbofish_ast);
8112+
if (tf_list->children > 0) {
8113+
zend_error_noreturn(E_COMPILE_ERROR,
8114+
"Type arguments cannot be applied to \"static\": "
8115+
"\"static\" is the runtime type and already carries its type arguments. "
8116+
"Use \"new static()\" to construct the exact runtime type, "
8117+
"or \"new self::<...>\" to apply type arguments to the enclosing class");
8118+
}
8119+
}
8120+
81008121
/* Reject turbofish on a known non-generic class at compile time so the
81018122
* error matches the type-position behavior (rather than waiting for
81028123
* VERIFY_GENERIC_ARGUMENTS to throw ArgumentCountError at runtime). */

0 commit comments

Comments
 (0)