Skip to content

Commit a50198d

Browse files
committed
Implement ??= operator
RFC: https://wiki.php.net/rfc/null_coalesce_equal_operator $a ??= $b is $a ?? ($a = $b), with the difference that $a is only evaluated once, to the degree that this is possible. In particular in $a[foo()] ?? $b function foo() is only ever called once. However, the variable access themselves will be reevaluated.
1 parent 50ddff9 commit a50198d

23 files changed

+1068
-517
lines changed

UPGRADING

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ PHP 7.4 UPGRADE NOTES
9090
This will enforce that $user->id can only be assigned integer and
9191
$user->name can only be assigned strings. For more information see the
9292
RFC: https://wiki.php.net/rfc/typed_properties_v2
93+
. Added support for coalesce assign (??=) operator. For example:
94+
95+
$array['key'] ??= computeDefault();
96+
// is roughly equivalent to
97+
if (!isset($array['key'])) {
98+
$array['key'] = computeDefault();
99+
}
100+
101+
RFC: https://wiki.php.net/rfc/null_coalesce_equal_operator
93102

94103
- FFI:
95104
. A new extension which provides a simple way to call native functions, access

Zend/tests/assign_coalesce_001.phpt

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
--TEST--
2+
Coalesce assign (??=): Basic behavior
3+
--FILE--
4+
<?php
5+
6+
// Refcounted values
7+
$foo = "fo";
8+
$foo .= "o";
9+
$bar = "ba";
10+
$bar .= "r";
11+
12+
// Identity function used to track single-evaluation
13+
function id($arg) {
14+
echo "id($arg)\n";
15+
return $arg;
16+
}
17+
18+
echo "Simple variables:\n";
19+
$a = 123;
20+
$a ??= 456;
21+
var_dump($a);
22+
23+
$b = null;
24+
$b ??= $foo;
25+
var_dump($b);
26+
27+
$c = $foo;
28+
$c ??= $bar;
29+
var_dump($c);
30+
31+
$d ??= $foo;
32+
var_dump($c);
33+
34+
echo "\nArrays:\n";
35+
$ary = [];
36+
$ary["foo"] ??= 123;
37+
$ary[$foo] ??= $bar;
38+
$ary[$bar] ??= $foo;
39+
var_dump($ary);
40+
41+
$ary = [];
42+
$ary[id($foo)] ??= 123;
43+
$ary[id($foo)] ??= $bar;
44+
$ary[id($bar)] ??= $foo;
45+
var_dump($ary);
46+
47+
echo "\nObjects:\n";
48+
$obj = new stdClass;
49+
$obj->foo ??= 123;
50+
$obj->$foo ??= $bar;
51+
$obj->$bar ??= $foo;
52+
var_dump($obj);
53+
54+
$obj = new stdClass;
55+
$obj->{id($foo)} ??= 123;
56+
$obj->{id($foo)} ??= $bar;
57+
$obj->{id($bar)} ??= $foo;
58+
var_dump($obj);
59+
60+
class Test {
61+
public static $foo;
62+
public static $bar;
63+
}
64+
65+
echo "\nStatic props:\n";
66+
Test::$foo ??= 123;
67+
Test::$$foo ??= $bar;
68+
Test::$$bar ??= $foo;
69+
var_dump(Test::$foo, Test::$bar);
70+
71+
Test::$foo = null;
72+
Test::$bar = null;
73+
Test::${id($foo)} ??= 123;
74+
Test::${id($foo)} ??= $bar;
75+
Test::${id($bar)} ??= $foo;
76+
var_dump(Test::$foo, Test::$bar);
77+
78+
?>
79+
--EXPECT--
80+
Simple variables:
81+
int(123)
82+
string(3) "foo"
83+
string(3) "foo"
84+
string(3) "foo"
85+
86+
Arrays:
87+
array(2) {
88+
["foo"]=>
89+
int(123)
90+
["bar"]=>
91+
string(3) "foo"
92+
}
93+
id(foo)
94+
id(foo)
95+
id(bar)
96+
array(2) {
97+
["foo"]=>
98+
int(123)
99+
["bar"]=>
100+
string(3) "foo"
101+
}
102+
103+
Objects:
104+
object(stdClass)#1 (2) {
105+
["foo"]=>
106+
int(123)
107+
["bar"]=>
108+
string(3) "foo"
109+
}
110+
id(foo)
111+
id(foo)
112+
id(bar)
113+
object(stdClass)#2 (2) {
114+
["foo"]=>
115+
int(123)
116+
["bar"]=>
117+
string(3) "foo"
118+
}
119+
120+
Static props:
121+
int(123)
122+
string(3) "foo"
123+
id(foo)
124+
id(foo)
125+
id(bar)
126+
int(123)
127+
string(3) "foo"

Zend/tests/assign_coalesce_002.phpt

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
--TEST--
2+
Coalesce assign (??=): Exception handling
3+
--FILE--
4+
<?php
5+
6+
$foo = "fo";
7+
$foo .= "o";
8+
$bar = "ba";
9+
$bar .= "r";
10+
11+
function id($arg) {
12+
echo "id($arg)\n";
13+
return $arg;
14+
}
15+
16+
function do_throw($msg) {
17+
throw new Exception($msg);
18+
}
19+
20+
$ary = [];
21+
try {
22+
$ary[id($foo)] ??= do_throw("ex1");
23+
} catch (Exception $e) {
24+
echo $e->getMessage(), "\n";
25+
}
26+
var_dump($ary);
27+
28+
class AA implements ArrayAccess {
29+
public function offsetExists($k) {
30+
return true;
31+
}
32+
public function &offsetGet($k) {
33+
$var = ["foo" => "bar"];
34+
return $var;
35+
}
36+
public function offsetSet($k,$v) {}
37+
public function offsetUnset($k) {}
38+
}
39+
40+
class Dtor {
41+
public function __destruct() {
42+
throw new Exception("dtor");
43+
}
44+
}
45+
46+
$ary = new AA;
47+
try {
48+
$ary[new Dtor][id($foo)] ??= $bar;
49+
} catch (Exception $e) {
50+
echo $e->getMessage(), "\n";
51+
}
52+
var_dump($foo);
53+
54+
class AA2 implements ArrayAccess {
55+
public function offsetExists($k) {
56+
return false;
57+
}
58+
public function offsetGet($k) {
59+
return null;
60+
}
61+
public function offsetSet($k,$v) {}
62+
public function offsetUnset($k) {}
63+
}
64+
65+
$ary = ["foo" => new AA2];
66+
try {
67+
$ary[id($foo)][new Dtor] ??= $bar;
68+
} catch (Exception $e) {
69+
echo $e->getMessage(), "\n";
70+
}
71+
var_dump($foo);
72+
73+
?>
74+
--EXPECT--
75+
id(foo)
76+
ex1
77+
array(0) {
78+
}
79+
id(foo)
80+
dtor
81+
string(3) "foo"
82+
id(foo)
83+
dtor
84+
string(3) "foo"

Zend/tests/assign_coalesce_003.phpt

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
--TEST--
2+
Coalesce assign (??=): ArrayAccess handling
3+
--FILE--
4+
<?php
5+
6+
function id($arg) {
7+
echo "id($arg)\n";
8+
return $arg;
9+
}
10+
11+
class AA implements ArrayAccess {
12+
public $data;
13+
public function __construct($data = []) {
14+
$this->data = $data;
15+
}
16+
public function &offsetGet($k) {
17+
echo "offsetGet($k)\n";
18+
return $this->data[$k];
19+
}
20+
public function offsetExists($k) {
21+
echo "offsetExists($k)\n";
22+
return array_key_exists($k, $this->data);
23+
}
24+
public function offsetSet($k,$v) {
25+
echo "offsetSet($k,$v)\n";
26+
$this->data[$k] = $v;
27+
}
28+
public function offsetUnset($k) { }
29+
}
30+
31+
$ary = new AA(["foo" => new AA, "null" => null]);
32+
33+
echo "[foo]\n";
34+
$ary["foo"] ??= "bar";
35+
36+
echo "[bar]\n";
37+
$ary["bar"] ??= "foo";
38+
39+
echo "[null]\n";
40+
$ary["null"] ??= "baz";
41+
42+
echo "[foo][bar]\n";
43+
$ary["foo"]["bar"] ??= "abc";
44+
45+
echo "[foo][bar]\n";
46+
$ary["foo"]["bar"] ??= "def";
47+
48+
?>
49+
--EXPECT--
50+
[foo]
51+
offsetExists(foo)
52+
offsetGet(foo)
53+
[bar]
54+
offsetExists(bar)
55+
offsetSet(bar,foo)
56+
[null]
57+
offsetExists(null)
58+
offsetGet(null)
59+
offsetSet(null,baz)
60+
[foo][bar]
61+
offsetExists(foo)
62+
offsetGet(foo)
63+
offsetExists(bar)
64+
offsetGet(foo)
65+
offsetSet(bar,abc)
66+
[foo][bar]
67+
offsetExists(foo)
68+
offsetGet(foo)
69+
offsetExists(bar)
70+
offsetGet(bar)

Zend/tests/assign_coalesce_004.phpt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
--TEST--
2+
Coalesce assign (??=): Non-writable variable
3+
--FILE--
4+
<?php
5+
6+
function foo() { return 123; }
7+
foo() ??= 456;
8+
9+
?>
10+
--EXPECTF--
11+
Fatal error: Can't use function return value in write context in %s on line %d

Zend/tests/assign_coalesce_005.phpt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--TEST--
2+
Coalesce assign (??=): Cannot reassign $this
3+
--FILE--
4+
<?php
5+
6+
class Test {
7+
public function foobar() {
8+
$this ??= 123;
9+
}
10+
}
11+
12+
?>
13+
--EXPECTF--
14+
Fatal error: Cannot re-assign $this in %s on line %d

Zend/zend_ast.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,6 +1672,7 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
16721672
EMPTY_SWITCH_DEFAULT_CASE();
16731673
}
16741674
break;
1675+
case ZEND_AST_ASSIGN_COALESCE: BINARY_OP(" \?\?= ", 90, 91, 90);
16751676
case ZEND_AST_BINARY_OP:
16761677
switch (ast->attr) {
16771678
case ZEND_ADD: BINARY_OP(" + ", 200, 200, 201);

Zend/zend_ast.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ enum _zend_ast_kind {
120120
ZEND_AST_INSTANCEOF,
121121
ZEND_AST_YIELD,
122122
ZEND_AST_COALESCE,
123+
ZEND_AST_ASSIGN_COALESCE,
123124

124125
ZEND_AST_STATIC,
125126
ZEND_AST_WHILE,

0 commit comments

Comments
 (0)