Skip to content

Commit 2e86157

Browse files
committed
Implement "Constructor Promotion"
1 parent 568592f commit 2e86157

12 files changed

+268
-10
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
Constructor promotion cannot be used inside an abstract constructor
3+
--FILE--
4+
<?php
5+
6+
abstract class Test {
7+
abstract public function __construct(public int $x);
8+
}
9+
10+
?>
11+
--EXPECTF--
12+
Fatal error: Cannot declare promoted property in an abstract constructor in %s on line %d
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
Constructor promotion only permits visibility modifiers
3+
--FILE--
4+
<?php
5+
6+
class Test {
7+
public function __construct(public static $x) {}
8+
}
9+
10+
?>
11+
--EXPECTF--
12+
Parse error: syntax error, unexpected 'static' (T_STATIC), expecting variable (T_VARIABLE) in %s on line %d

Zend/tests/ctor_promotion_basic.phpt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
Constructor promotion (basic example)
3+
--FILE--
4+
<?php
5+
6+
class Point {
7+
public function __construct(public int $x, public int $y, public int $z) {}
8+
}
9+
10+
$point = new Point(1, 2, 3);
11+
12+
// Check that properties really are typed.
13+
try {
14+
$point->x = "foo";
15+
} catch (TypeError $e) {
16+
echo $e->getMessage(), "\n";
17+
}
18+
19+
?>
20+
--EXPECT--
21+
Cannot assign string to property Point::$x of type int
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
Type of promoted property may not be callable
3+
--FILE--
4+
<?php
5+
6+
class Test {
7+
public function __construct(public callable $callable) {}
8+
}
9+
10+
?>
11+
--EXPECTF--
12+
Fatal error: Property Test::$callable cannot have type callable in %s on line %d
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
--TEST--
2+
Constructor promotion with default values
3+
--FILE--
4+
<?php
5+
6+
class Point {
7+
public function __construct(
8+
public float $x = 0.0,
9+
public float $y = 1.0,
10+
public float $z = 2.0
11+
) {}
12+
}
13+
14+
var_dump(new Point(10.0));
15+
var_dump(new Point(10.0, 11.0));
16+
var_dump(new Point(10.0, 11.0, 12.0));
17+
18+
?>
19+
--EXPECT--
20+
object(Point)#1 (3) {
21+
["x"]=>
22+
float(10)
23+
["y"]=>
24+
float(1)
25+
["z"]=>
26+
float(2)
27+
}
28+
object(Point)#1 (3) {
29+
["x"]=>
30+
float(10)
31+
["y"]=>
32+
float(11)
33+
["z"]=>
34+
float(2)
35+
}
36+
object(Point)#1 (3) {
37+
["x"]=>
38+
float(10)
39+
["y"]=>
40+
float(11)
41+
["z"]=>
42+
float(12)
43+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
Constructor promotion cannot be used inside an abstract constructor (interface variant)
3+
--FILE--
4+
<?php
5+
6+
interface Test {
7+
public function __construct(public int $x);
8+
}
9+
10+
?>
11+
--EXPECTF--
12+
Fatal error: Cannot declare promoted property in an abstract constructor in %s on line %d

Zend/tests/ctor_promotion_mixing.phpt

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
--TEST--
2+
Constructor promotiong mixed with other properties, parameters and code
3+
--FILE--
4+
<?php
5+
6+
class Test {
7+
public string $prop2;
8+
9+
public function __construct(public string $prop1 = "", $param2 = "") {
10+
$this->prop2 = $prop1 . $param2;
11+
}
12+
}
13+
14+
var_dump(new Test("Foo", "Bar"));
15+
echo "\n";
16+
echo new ReflectionClass(Test::class), "\n";
17+
18+
?>
19+
--EXPECTF--
20+
object(Test)#1 (2) {
21+
["prop2"]=>
22+
string(6) "FooBar"
23+
["prop1"]=>
24+
string(3) "Foo"
25+
}
26+
27+
Class [ <user> class Test ] {
28+
@@ %s
29+
30+
- Constants [0] {
31+
}
32+
33+
- Static properties [0] {
34+
}
35+
36+
- Static methods [0] {
37+
}
38+
39+
- Properties [2] {
40+
Property [ <default> public $prop2 ]
41+
Property [ <default> public $prop1 ]
42+
}
43+
44+
- Methods [1] {
45+
Method [ <user, ctor> public method __construct ] {
46+
@@ %s
47+
48+
- Parameters [2] {
49+
Parameter #0 [ <optional> string $prop1 = '' ]
50+
Parameter #1 [ <optional> $param2 = '' ]
51+
}
52+
}
53+
}
54+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
Constructor promotion can only be used in constructors ... duh
3+
--FILE--
4+
<?php
5+
6+
class Test {
7+
public function foobar(public int $x, public int $y) {}
8+
}
9+
10+
?>
11+
--EXPECTF--
12+
Fatal error: Cannot declare promoted property outside a constructor in %s on line %d
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
Constructor promotion with null default, requires an explicitly nullable type
3+
--FILE--
4+
<?php
5+
6+
class Test {
7+
public function __construct(public int $x = null) {}
8+
}
9+
10+
?>
11+
--EXPECTF--
12+
Fatal error: Cannot use null as default value for parameter $x of type int in %s on line %d

Zend/zend_compile.c

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5761,6 +5761,8 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
57615761
zend_string *name = zval_make_interned_string(zend_ast_get_zval(var_ast));
57625762
zend_bool is_ref = (param_ast->attr & ZEND_PARAM_REF) != 0;
57635763
zend_bool is_variadic = (param_ast->attr & ZEND_PARAM_VARIADIC) != 0;
5764+
uint32_t visibility =
5765+
param_ast->attr & (ZEND_ACC_PUBLIC|ZEND_ACC_PROTECTED|ZEND_ACC_PRIVATE);
57645766

57655767
znode var_node, default_node;
57665768
zend_uchar opcode;
@@ -5829,16 +5831,16 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
58295831

58305832
if (type_ast) {
58315833
uint32_t default_type = default_ast ? Z_TYPE(default_node.u.constant) : IS_UNDEF;
5834+
zend_bool force_nullable = default_type == IS_NULL && !visibility;
58325835

58335836
op_array->fn_flags |= ZEND_ACC_HAS_TYPE_HINTS;
5834-
arg_info->type = zend_compile_typename(
5835-
type_ast, default_type == IS_NULL, /* use_arena */ 0);
5837+
arg_info->type = zend_compile_typename(type_ast, force_nullable, /* use_arena */ 0);
58365838

58375839
if (ZEND_TYPE_FULL_MASK(arg_info->type) & MAY_BE_VOID) {
58385840
zend_error_noreturn(E_COMPILE_ERROR, "void cannot be used as a parameter type");
58395841
}
58405842

5841-
if (default_type > IS_NULL && default_type != IS_CONSTANT_AST
5843+
if (default_type != IS_UNDEF && default_type != IS_CONSTANT_AST && !force_nullable
58425844
&& !zend_is_valid_default_value(arg_info->type, &default_node.u.constant)) {
58435845
zend_string *type_str = zend_type_to_string(arg_info->type);
58445846
zend_error_noreturn(E_COMPILE_ERROR,
@@ -5863,6 +5865,41 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
58635865
opline->op2.num = type_ast ?
58645866
ZEND_TYPE_FULL_MASK(arg_info->type) : MAY_BE_ANY;
58655867
}
5868+
5869+
if (visibility) {
5870+
zend_op_array *op_array = CG(active_op_array);
5871+
zend_class_entry *scope = op_array->scope;
5872+
zend_bool is_ctor =
5873+
scope && zend_string_equals_literal_ci(op_array->function_name, "__construct");
5874+
if (!is_ctor) {
5875+
zend_error_noreturn(E_COMPILE_ERROR,
5876+
"Cannot declare promoted property outside a constructor");
5877+
}
5878+
if ((op_array->fn_flags & ZEND_ACC_ABSTRACT)
5879+
|| (scope->ce_flags & ZEND_ACC_INTERFACE)) {
5880+
zend_error_noreturn(E_COMPILE_ERROR,
5881+
"Cannot declare promoted property in an abstract constructor");
5882+
}
5883+
if (ZEND_TYPE_FULL_MASK(arg_info->type) & MAY_BE_CALLABLE) {
5884+
zend_string *str = zend_type_to_string(arg_info->type);
5885+
zend_error_noreturn(E_COMPILE_ERROR,
5886+
"Property %s::$%s cannot have type %s",
5887+
ZSTR_VAL(scope->name), ZSTR_VAL(name), ZSTR_VAL(str));
5888+
}
5889+
5890+
/* Always use uninitialized as the default. */
5891+
zval default_value;
5892+
ZVAL_UNDEF(&default_value);
5893+
5894+
/* Recompile the type, as it has different memory management requirements. */
5895+
zend_type type = ZEND_TYPE_INIT_NONE(0);
5896+
if (type_ast) {
5897+
type = zend_compile_typename(type_ast, /* force_allow_null */ 0, /* use_arena */ 1);
5898+
}
5899+
5900+
zend_declare_typed_property(
5901+
scope, name, &default_value, visibility, /* doc_comment */ NULL, type);
5902+
}
58665903
}
58675904

58685905
/* These are assigned at the end to avoid uninitialized memory in case of an error */
@@ -5874,6 +5911,27 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
58745911
op_array->num_args--;
58755912
}
58765913
zend_set_function_arg_flags((zend_function*)op_array);
5914+
5915+
for (i = 0; i < list->children; i++) {
5916+
zend_ast *param_ast = list->child[i];
5917+
uint32_t visibility =
5918+
param_ast->attr & (ZEND_ACC_PUBLIC|ZEND_ACC_PROTECTED|ZEND_ACC_PRIVATE);
5919+
if (!visibility) {
5920+
continue;
5921+
}
5922+
5923+
/* Emit $this->prop = $prop for promoted properties. */
5924+
zend_string *name = zend_ast_get_str(param_ast->child[1]);
5925+
znode name_node, value_node;
5926+
name_node.op_type = IS_CONST;
5927+
ZVAL_STR_COPY(&name_node.u.constant, name);
5928+
value_node.op_type = IS_CV;
5929+
value_node.u.op.var = lookup_cv(name);
5930+
5931+
zend_op *opline = zend_emit_op(NULL, ZEND_ASSIGN_OBJ, NULL, &name_node);
5932+
opline->extended_value = zend_alloc_cache_slots(3);
5933+
zend_emit_op_data(&value_node);
5934+
}
58775935
}
58785936
/* }}} */
58795937

Zend/zend_compile.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -863,8 +863,9 @@ ZEND_API zend_string *zend_type_to_string(zend_type type);
863863
#define ZEND_FETCH_CLASS_ALLOW_UNLINKED 0x0400
864864
#define ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED 0x0800
865865

866-
#define ZEND_PARAM_REF (1<<0)
867-
#define ZEND_PARAM_VARIADIC (1<<1)
866+
/* These should not clash with ZEND_ACC_(PUBLIC|PROTECTED|PRIVATE) */
867+
#define ZEND_PARAM_REF (1<<3)
868+
#define ZEND_PARAM_VARIADIC (1<<4)
868869

869870
#define ZEND_NAME_FQ 0
870871
#define ZEND_NAME_NOT_FQ 1

Zend/zend_language_parser.y

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
259259
%type <ast> inline_function union_type
260260

261261
%type <num> returns_ref function fn is_reference is_variadic variable_modifiers
262-
%type <num> method_modifiers non_empty_member_modifiers member_modifier
262+
%type <num> method_modifiers non_empty_member_modifiers member_modifier optional_visibility_modifier
263263
%type <num> class_modifiers class_modifier use_type backup_fn_flags
264264

265265
%type <ptr> backup_lex_pos
@@ -645,11 +645,20 @@ non_empty_parameter_list:
645645
{ $$ = zend_ast_list_add($1, $3); }
646646
;
647647

648+
optional_visibility_modifier:
649+
%empty { $$ = 0; }
650+
| T_PUBLIC { $$ = ZEND_ACC_PUBLIC; }
651+
| T_PROTECTED { $$ = ZEND_ACC_PROTECTED; }
652+
| T_PRIVATE { $$ = ZEND_ACC_PRIVATE; }
653+
;
654+
648655
parameter:
649-
optional_type_without_static is_reference is_variadic T_VARIABLE
650-
{ $$ = zend_ast_create_ex(ZEND_AST_PARAM, $2 | $3, $1, $4, NULL); }
651-
| optional_type_without_static is_reference is_variadic T_VARIABLE '=' expr
652-
{ $$ = zend_ast_create_ex(ZEND_AST_PARAM, $2 | $3, $1, $4, $6); }
656+
optional_visibility_modifier optional_type_without_static
657+
is_reference is_variadic T_VARIABLE
658+
{ $$ = zend_ast_create_ex(ZEND_AST_PARAM, $1 | $3 | $4, $2, $5, NULL); }
659+
| optional_visibility_modifier optional_type_without_static
660+
is_reference is_variadic T_VARIABLE '=' expr
661+
{ $$ = zend_ast_create_ex(ZEND_AST_PARAM, $1 | $3 | $4, $2, $5, $7); }
653662
;
654663

655664

0 commit comments

Comments
 (0)