Skip to content

Commit f7c5e3f

Browse files
committed
Support arbitrary expressions in property initializations and parameter defaults
See https://wiki.php.net/rfc/new_in_initializers
1 parent 3a0d193 commit f7c5e3f

File tree

3 files changed

+175
-13
lines changed

3 files changed

+175
-13
lines changed

src/main/php/lang/ast/emit/PHP.class.php

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php namespace lang\ast\emit;
22

3-
use lang\ast\nodes\{InstanceExpression, ScopeExpression, Variable, Block};
3+
use lang\ast\Code;
4+
use lang\ast\nodes\{InstanceExpression, ScopeExpression, Variable, Literal, ArrayLiteral, Block};
45
use lang\ast\types\{IsUnion, IsFunction, IsArray, IsMap};
56
use lang\ast\{Emitter, Node, Type};
67

@@ -30,6 +31,31 @@ protected function declaration($name) {
3031
return substr($name, strrpos($name, '\\') + 1);
3132
}
3233

34+
/**
35+
* Returns whether a given node is a so-called "static scalar" expression:
36+
*
37+
* - Any literal
38+
* - Arrays where all members are literals
39+
* - Scope expressions with literal members (self::class, T::const)
40+
*
41+
* @see https://wiki.php.net/rfc/new_in_initializers
42+
* @param lang.ast.Node $node
43+
* @return bool
44+
*/
45+
protected function staticScalar($node) {
46+
if ($node instanceof Literal) {
47+
return true;
48+
} else if ($node instanceof ArrayLiteral) {
49+
foreach ($node->values as $node) {
50+
if (!$this->staticScalar($node)) return false;
51+
}
52+
return true;
53+
} else if ($node instanceof ScopeExpression) {
54+
return $node->member instanceof Literal;
55+
}
56+
return false;
57+
}
58+
3359
/**
3460
* As of PHP 7.4: Property type declarations support all type declarations
3561
* supported by PHP with the exception of void and callable.
@@ -88,6 +114,21 @@ protected function enclose($result, $node, $signature, $emit) {
88114
$result->locals= array_pop($result->stack);
89115
}
90116

117+
/**
118+
* Emits local initializations
119+
*
120+
* @param lang.ast.Result $result
121+
* @param [:lang.ast.Node] $init
122+
* @return void
123+
*/
124+
protected function emitInitializations($result, $init) {
125+
foreach ($init as $assign => $expression) {
126+
$result->out->write($assign.'=');
127+
$this->emitOne($result, $expression);
128+
$result->out->write(';');
129+
}
130+
}
131+
91132
/**
92133
* Convert blocks to IIFEs to allow a list of statements where PHP syntactically
93134
* doesn't, e.g. `fn`-style lambdas or match expressions.
@@ -235,8 +276,13 @@ protected function emitParameter($result, $parameter) {
235276
$result->out->write(($parameter->reference ? '&' : '').'$'.$parameter->name);
236277
}
237278
if ($parameter->default) {
238-
$result->out->write('=');
239-
$this->emitOne($result, $parameter->default);
279+
if ($this->staticScalar($parameter->default)) {
280+
$result->out->write('=');
281+
$this->emitOne($result, $parameter->default);
282+
} else {
283+
$result->out->write('=null');
284+
$result->locals['$']['null === $'.$parameter->name.' && $'.$parameter->name]= $parameter->default;
285+
}
240286
}
241287
$result->locals[$parameter->name]= true;
242288
}
@@ -297,6 +343,7 @@ protected function emitLambda($result, $lambda) {
297343

298344
protected function emitClass($result, $class) {
299345
array_unshift($result->meta, []);
346+
$result->locals= ['$' => [], '@' => []];
300347

301348
$result->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name));
302349
$class->parent && $result->out->write(' extends '.$class->parent);
@@ -306,7 +353,16 @@ protected function emitClass($result, $class) {
306353
$this->emitOne($result, $member);
307354
}
308355

356+
// Create constructor for property initializations to non-static scalars
357+
// which have not already been emitted inside constructor
358+
if ($result->locals['$']) {
359+
$result->out->write('public function __construct() {');
360+
$this->emitInitializations($result, $result->locals['$']);
361+
$result->out->write('}');
362+
}
363+
309364
$result->out->write('static function __init() {');
365+
$this->emitInitializations($result, $result->locals['@']);
310366
$this->emitMeta($result, $class->name, $class->annotations, $class->comment);
311367
$result->out->write('}} '.$class->name.'::__init();');
312368
}
@@ -452,15 +508,31 @@ protected function emitProperty($result, $property) {
452508

453509
$result->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name);
454510
if (isset($property->expression)) {
455-
$result->out->write('=');
456-
$this->emitOne($result, $property->expression);
511+
if ($this->staticScalar($property->expression)) {
512+
$result->out->write('=');
513+
$this->emitOne($result, $property->expression);
514+
} else if (in_array('static', $property->modifiers)) {
515+
$result->locals['@']['self::$'.$property->name]= $property->expression;
516+
} else {
517+
$result->locals['$']['$this->'.$property->name]= $property->expression;
518+
}
457519
}
458520
$result->out->write(';');
459521
}
460522

461523
protected function emitMethod($result, $method) {
524+
525+
// Include non-static initializations for members in constructor, removing
526+
// them to signal emitClass() no constructor needs to be generated.
527+
if ('__construct' === $method->name && isset($result->locals['$'])) {
528+
$locals= ['this' => true, '$' => $result->locals['$']];
529+
$result->locals['$']= [];
530+
} else {
531+
$locals= ['this' => true, '$' => []];
532+
}
462533
$result->stack[]= $result->locals;
463-
$result->locals= ['this' => true];
534+
$result->locals= $locals;
535+
464536
$meta= [
465537
DETAIL_RETURNS => $method->signature->returns ? $method->signature->returns->name() : 'var',
466538
DETAIL_ANNOTATIONS => $method->annotations,
@@ -469,11 +541,14 @@ protected function emitMethod($result, $method) {
469541
DETAIL_ARGUMENTS => []
470542
];
471543

472-
$declare= $promote= $params= '';
544+
$result->out->write(implode(' ', $method->modifiers).' function '.$method->name);
545+
$this->emitSignature($result, $method->signature);
546+
547+
$promoted= '';
473548
foreach ($method->signature->parameters as $param) {
474549
if (isset($param->promote)) {
475-
$declare.= $param->promote.' $'.$param->name.';';
476-
$promote.= '$this->'.$param->name.'= '.($param->reference ? '&$' : '$').$param->name.';';
550+
$promoted.= $param->promote.' $'.$param->name.';';
551+
$result->locals['$']['$this->'.$param->name]= new Code(($param->reference ? '&$' : '$').$param->name);
477552
$result->meta[0][self::PROPERTY][$param->name]= [
478553
DETAIL_RETURNS => $param->type ? $param->type->name() : 'var',
479554
DETAIL_ANNOTATIONS => [],
@@ -485,18 +560,17 @@ protected function emitMethod($result, $method) {
485560
$meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations;
486561
$meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var';
487562
}
488-
$result->out->write($declare);
489-
$result->out->write(implode(' ', $method->modifiers).' function '.$method->name);
490-
$this->emitSignature($result, $method->signature);
491563

492564
if (null === $method->body) {
493565
$result->out->write(';');
494566
} else {
495-
$result->out->write(' {'.$promote);
567+
$result->out->write(' {');
568+
$this->emitInitializations($result, $result->locals['$']);
496569
$this->emitAll($result, $method->body);
497570
$result->out->write('}');
498571
}
499572

573+
$result->out->write($promoted);
500574
$result->meta[0][self::METHOD][$method->name]= $meta;
501575
$result->locals= array_pop($result->stack);
502576
}

src/test/php/lang/ast/unittest/emit/Handle.class.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ class Handle implements \IDisposable {
99

1010
public function __construct($id) { $this->id= $id; }
1111

12+
public function redirect($id) {
13+
$this->id= $id;
14+
}
15+
1216
public function read($bytes= 8192) {
1317
self::$called[]= 'read@'.$this->id;
1418

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php namespace lang\ast\unittest\emit;
2+
3+
use lang\IllegalArgumentException;
4+
use unittest\{Assert, Expect, Test, Values};
5+
6+
/**
7+
* New in initializers
8+
*
9+
* @see https://wiki.php.net/rfc/new_in_initializers
10+
*/
11+
class InitializeWithNewTest extends EmittingTest {
12+
13+
#[Test]
14+
public function property() {
15+
$r= $this->run('use lang\ast\unittest\emit\Handle; class <T> {
16+
private $h= new Handle(0);
17+
18+
public function run() {
19+
return $this->h;
20+
}
21+
}');
22+
Assert::equals(new Handle(0), $r);
23+
}
24+
25+
#[Test]
26+
public function property_initialization_accessible_inside_constructor() {
27+
$r= $this->run('use lang\ast\unittest\emit\Handle; class <T> {
28+
private $h= new Handle(0);
29+
30+
public function __construct() {
31+
$this->h->redirect(1);
32+
}
33+
34+
public function run() {
35+
return $this->h;
36+
}
37+
}');
38+
Assert::equals(new Handle(1), $r);
39+
}
40+
41+
#[Test]
42+
public function promoted_property() {
43+
$r= $this->run('use lang\ast\unittest\emit\Handle; class <T> {
44+
public function __construct(private $h= new Handle(0)) { }
45+
46+
public function run() {
47+
return $this->h;
48+
}
49+
}');
50+
Assert::equals(new Handle(0), $r);
51+
}
52+
53+
#[Test]
54+
public function parameter_default_when_omitted() {
55+
$r= $this->run('use lang\ast\unittest\emit\Handle; class <T> {
56+
public function run($h= new Handle(0)) {
57+
return $h;
58+
}
59+
}');
60+
Assert::equals(new Handle(0), $r);
61+
}
62+
63+
#[Test]
64+
public function parameter_default_when_passed() {
65+
$r= $this->run('use lang\ast\unittest\emit\Handle; class <T> {
66+
public function run($h= new Handle(0)) {
67+
return $h;
68+
}
69+
}', new Handle(1));
70+
Assert::equals(new Handle(1), $r);
71+
}
72+
73+
#[Test]
74+
public function property_reference_as_parameter_default() {
75+
$r= $this->run('use lang\ast\unittest\emit\Handle; class <T> {
76+
private static $h= new Handle(0);
77+
78+
public function run($h= self::$h) {
79+
return $h;
80+
}
81+
}');
82+
Assert::equals(new Handle(0), $r);
83+
}
84+
}

0 commit comments

Comments
 (0)