Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Implement "Constructor Promotion"
  • Loading branch information
nikic committed Jun 5, 2020
commit a7c7d9180edf664bf678df583bf69676e33ecd69
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the RFC code example the trailing comma is allowed, but not here.

Which is correct?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the subject of different RFC

Copy link

@TomasVotruba TomasVotruba Jun 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

) {}
}

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)
}
10 changes: 10 additions & 0 deletions Zend/tests/ctor_promotion_free_function.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--TEST--
Constructor promotion cannot be used in a free function
--FILE--
<?php

function __construct(public $prop) {}

?>
--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_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 @@ -5790,6 +5790,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 @@ -5862,16 +5864,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 @@ -5896,6 +5898,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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could add a test of global functions/closures - I'd guess that the following snippet probably has a misleading error message (not sure what scope->properties_info is)

<?php
function __construct(public $x) {}

LGTM otherwise

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test in 717134e, error message seems fine. Or did you expect something different there?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry, I missed that you had function __construct() in particular in mind here. Changed the name in 7139cdc.

Still works fine though, because is_ctor above includes a check that scope is not null.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, didn't see the scope part of the check

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 @@ -5907,6 +5952,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
Loading