Skip to content

Commit

Permalink
Implement "Constructor Promotion"
Browse files Browse the repository at this point in the history
  • Loading branch information
nikic committed Mar 26, 2020
1 parent e8e09b6 commit 4200794
Show file tree
Hide file tree
Showing 16 changed files with 345 additions and 10 deletions.
12 changes: 12 additions & 0 deletions Zend/tests/ctor_promotion_abstract.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Constructor promotion cannot be used inside an abstract constructor
--FILE--
<?php

abstract class Test {
abstract public function __construct(public int $x);
}

?>
--EXPECTF--
Fatal error: Cannot declare promoted property in an abstract constructor in %s on line %d
12 changes: 12 additions & 0 deletions Zend/tests/ctor_promotion_additional_modifiers.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Constructor promotion only permits visibility modifiers
--FILE--
<?php

class Test {
public function __construct(public static $x) {}
}

?>
--EXPECTF--
Parse error: syntax error, unexpected 'static' (T_STATIC), expecting variable (T_VARIABLE) in %s on line %d
21 changes: 21 additions & 0 deletions Zend/tests/ctor_promotion_basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--TEST--
Constructor promotion (basic example)
--FILE--
<?php

class Point {
public function __construct(public int $x, public int $y, public int $z) {}
}

$point = new Point(1, 2, 3);

// Check that properties really are typed.
try {
$point->x = "foo";
} catch (TypeError $e) {
echo $e->getMessage(), "\n";
}

?>
--EXPECT--
Cannot assign string to property Point::$x of type int
20 changes: 20 additions & 0 deletions Zend/tests/ctor_promotion_by_ref.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--TEST--
Constructor promotion of by-ref parameter
--FILE--
<?php

class Ary {
public function __construct(public array &$array) {}
}

$array = [];
$ary = new Ary($array);
$array[] = 42;
var_dump($ary->array);

?>
--EXPECT--
array(1) {
[0]=>
int(42)
}
12 changes: 12 additions & 0 deletions Zend/tests/ctor_promotion_callable_type.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Type of promoted property may not be callable
--FILE--
<?php

class Test {
public function __construct(public callable $callable) {}
}

?>
--EXPECTF--
Fatal error: Property Test::$callable cannot have type callable in %s on line %d
43 changes: 43 additions & 0 deletions Zend/tests/ctor_promotion_defaults.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
--TEST--
Constructor promotion with default values
--FILE--
<?php

class Point {
public function __construct(
public float $x = 0.0,
public float $y = 1.0,
public float $z = 2.0
) {}
}

var_dump(new Point(10.0));
var_dump(new Point(10.0, 11.0));
var_dump(new Point(10.0, 11.0, 12.0));

?>
--EXPECT--
object(Point)#1 (3) {
["x"]=>
float(10)
["y"]=>
float(1)
["z"]=>
float(2)
}
object(Point)#1 (3) {
["x"]=>
float(10)
["y"]=>
float(11)
["z"]=>
float(2)
}
object(Point)#1 (3) {
["x"]=>
float(10)
["y"]=>
float(11)
["z"]=>
float(12)
}
12 changes: 12 additions & 0 deletions Zend/tests/ctor_promotion_interface.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Constructor promotion cannot be used inside an abstract constructor (interface variant)
--FILE--
<?php

interface Test {
public function __construct(public int $x);
}

?>
--EXPECTF--
Fatal error: Cannot declare promoted property in an abstract constructor in %s on line %d
54 changes: 54 additions & 0 deletions Zend/tests/ctor_promotion_mixing.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
--TEST--
Constructor promotiong mixed with other properties, parameters and code
--FILE--
<?php

class Test {
public string $prop2;

public function __construct(public string $prop1 = "", $param2 = "") {
$this->prop2 = $prop1 . $param2;
}
}

var_dump(new Test("Foo", "Bar"));
echo "\n";
echo new ReflectionClass(Test::class), "\n";

?>
--EXPECTF--
object(Test)#1 (2) {
["prop2"]=>
string(6) "FooBar"
["prop1"]=>
string(3) "Foo"
}

Class [ <user> class Test ] {
@@ %s

- Constants [0] {
}

- Static properties [0] {
}

- Static methods [0] {
}

- Properties [2] {
Property [ <default> public $prop2 ]
Property [ <default> public $prop1 ]
}

- Methods [1] {
Method [ <user, ctor> public method __construct ] {
@@ %s

- Parameters [2] {
Parameter #0 [ <optional> string $prop1 = '' ]
Parameter #1 [ <optional> $param2 = '' ]
}
}
}
}
12 changes: 12 additions & 0 deletions Zend/tests/ctor_promotion_not_a_ctor.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Constructor promotion can only be used in constructors ... duh
--FILE--
<?php

class Test {
public function foobar(public int $x, public int $y) {}
}

?>
--EXPECTF--
Fatal error: Cannot declare promoted property outside a constructor in %s on line %d
12 changes: 12 additions & 0 deletions Zend/tests/ctor_promotion_null_default.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Constructor promotion with null default, requires an explicitly nullable type
--FILE--
<?php

class Test {
public function __construct(public int $x = null) {}
}

?>
--EXPECTF--
Fatal error: Cannot use null as default value for parameter $x of type int in %s on line %d
14 changes: 14 additions & 0 deletions Zend/tests/ctor_promotion_repeated_prop.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
--TEST--
Clash between promoted and explicit property
--FILE--
<?php

class Test {
public $prop;

public function __construct(public $prop) {}
}

?>
--EXPECTF--
Fatal error: Cannot redeclare Test::$prop in %s on line %d
21 changes: 21 additions & 0 deletions Zend/tests/ctor_promotion_trait.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--TEST--
Constructor promotion can be used inside a trait
--FILE--
<?php

trait Test {
public function __construct(public $prop) {}
}

class Test2 {
use Test;
}

var_dump(new Test2(42));

?>
--EXPECT--
object(Test2)#1 (1) {
["prop"]=>
int(42)
}
12 changes: 12 additions & 0 deletions Zend/tests/ctor_promotion_variadic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
Cannot use constructor promotion with variadic parameter
--FILE--
<?php

class Test {
public function __construct(public string ...$strings) {}
}

?>
--EXPECTF--
Fatal error: Cannot declare variadic promoted property in %s on line %d
74 changes: 71 additions & 3 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -5761,6 +5761,8 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
zend_string *name = zval_make_interned_string(zend_ast_get_zval(var_ast));
zend_bool is_ref = (param_ast->attr & ZEND_PARAM_REF) != 0;
zend_bool is_variadic = (param_ast->attr & ZEND_PARAM_VARIADIC) != 0;
uint32_t visibility =
param_ast->attr & (ZEND_ACC_PUBLIC|ZEND_ACC_PROTECTED|ZEND_ACC_PRIVATE);

znode var_node, default_node;
zend_uchar opcode;
Expand Down Expand Up @@ -5829,16 +5831,16 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall

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

op_array->fn_flags |= ZEND_ACC_HAS_TYPE_HINTS;
arg_info->type = zend_compile_typename(
type_ast, default_type == IS_NULL, /* use_arena */ 0);
arg_info->type = zend_compile_typename(type_ast, force_nullable, /* use_arena */ 0);

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

if (default_type > IS_NULL && default_type != IS_CONSTANT_AST
if (default_type != IS_UNDEF && default_type != IS_CONSTANT_AST && !force_nullable
&& !zend_is_valid_default_value(arg_info->type, &default_node.u.constant)) {
zend_string *type_str = zend_type_to_string(arg_info->type);
zend_error_noreturn(E_COMPILE_ERROR,
Expand All @@ -5863,6 +5865,49 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
opline->op2.num = type_ast ?
ZEND_TYPE_FULL_MASK(arg_info->type) : MAY_BE_ANY;
}

if (visibility) {
zend_op_array *op_array = CG(active_op_array);
zend_class_entry *scope = op_array->scope;
zend_bool is_ctor =
scope && zend_string_equals_literal_ci(op_array->function_name, "__construct");
if (!is_ctor) {
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot declare promoted property outside a constructor");
}
if ((op_array->fn_flags & ZEND_ACC_ABSTRACT)
|| (scope->ce_flags & ZEND_ACC_INTERFACE)) {
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot declare promoted property in an abstract constructor");
}
if (is_variadic) {
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot declare variadic promoted property");
}
if (zend_hash_exists(&scope->properties_info, name)) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot redeclare %s::$%s",
ZSTR_VAL(scope->name), ZSTR_VAL(name));
}
if (ZEND_TYPE_FULL_MASK(arg_info->type) & MAY_BE_CALLABLE) {
zend_string *str = zend_type_to_string(arg_info->type);
zend_error_noreturn(E_COMPILE_ERROR,
"Property %s::$%s cannot have type %s",
ZSTR_VAL(scope->name), ZSTR_VAL(name), ZSTR_VAL(str));
}

/* Always use uninitialized as the default. */
zval default_value;
ZVAL_UNDEF(&default_value);

/* Recompile the type, as it has different memory management requirements. */
zend_type type = ZEND_TYPE_INIT_NONE(0);
if (type_ast) {
type = zend_compile_typename(type_ast, /* force_allow_null */ 0, /* use_arena */ 1);
}

zend_declare_typed_property(
scope, name, &default_value, visibility, /* doc_comment */ NULL, type);
}
}

/* These are assigned at the end to avoid uninitialized memory in case of an error */
Expand All @@ -5874,6 +5919,29 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
op_array->num_args--;
}
zend_set_function_arg_flags((zend_function*)op_array);

for (i = 0; i < list->children; i++) {
zend_ast *param_ast = list->child[i];
zend_bool is_ref = (param_ast->attr & ZEND_PARAM_REF) != 0;
uint32_t visibility =
param_ast->attr & (ZEND_ACC_PUBLIC|ZEND_ACC_PROTECTED|ZEND_ACC_PRIVATE);
if (!visibility) {
continue;
}

/* Emit $this->prop = $prop for promoted properties. */
zend_string *name = zend_ast_get_str(param_ast->child[1]);
znode name_node, value_node;
name_node.op_type = IS_CONST;
ZVAL_STR_COPY(&name_node.u.constant, name);
value_node.op_type = IS_CV;
value_node.u.op.var = lookup_cv(name);

zend_op *opline = zend_emit_op(NULL,
is_ref ? ZEND_ASSIGN_OBJ_REF : ZEND_ASSIGN_OBJ, NULL, &name_node);
opline->extended_value = zend_alloc_cache_slots(3);
zend_emit_op_data(&value_node);
}
}
/* }}} */

Expand Down
5 changes: 3 additions & 2 deletions Zend/zend_compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -863,8 +863,9 @@ ZEND_API zend_string *zend_type_to_string(zend_type type);
#define ZEND_FETCH_CLASS_ALLOW_UNLINKED 0x0400
#define ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED 0x0800

#define ZEND_PARAM_REF (1<<0)
#define ZEND_PARAM_VARIADIC (1<<1)
/* These should not clash with ZEND_ACC_(PUBLIC|PROTECTED|PRIVATE) */
#define ZEND_PARAM_REF (1<<3)
#define ZEND_PARAM_VARIADIC (1<<4)

#define ZEND_NAME_FQ 0
#define ZEND_NAME_NOT_FQ 1
Expand Down
Loading

0 comments on commit 4200794

Please sign in to comment.