Skip to content

Commit 9be29fa

Browse files
authored
Merge pull request #106 from xp-framework/feature/php-enum-support
Compile PHP enums to PHP 7/8 lookalikes, PHP 8.1 native
2 parents 15f343d + d4476da commit 9be29fa

File tree

9 files changed

+623
-9
lines changed

9 files changed

+623
-9
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"keywords": ["module", "xp"],
88
"require" : {
99
"xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0",
10-
"xp-framework/ast": "^7.0",
10+
"xp-framework/ast": "^7.1",
1111
"php" : ">=7.0.0"
1212
},
1313
"require-dev" : {

src/main/php/lang/ast/Result.class.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
<?php namespace lang\ast;
22

3+
use lang\ast\emit\{Declaration, Reflection};
4+
35
class Result {
46
public $out;
57
public $codegen;
68
public $line= 1;
79
public $meta= [];
810
public $locals= [];
911
public $stack= [];
12+
public $type= [];
1013

1114
/**
1215
* Starts a result stream, including a preamble
@@ -28,4 +31,20 @@ public function __construct($out, $preamble= '<?php ') {
2831
public function temp() {
2932
return '$'.$this->codegen->symbol();
3033
}
34+
35+
/**
36+
* Looks up a given type
37+
*
38+
* @param string $type
39+
* @return lang.ast.emit.Type
40+
*/
41+
public function lookup($type) {
42+
if ('self' === $type || 'static' === $type || $type === $this->type[0]->name) {
43+
return new Declaration($this->type[0], $this);
44+
} else if ('parent' === $type) {
45+
return $this->lookup($this->type[0]->parent);
46+
} else {
47+
return new Reflection($type);
48+
}
49+
}
3150
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php namespace lang\ast\emit;
2+
3+
use lang\ast\nodes\{EnumCase, Property};
4+
5+
class Declaration extends Type {
6+
private $type, $result;
7+
8+
static function __static() { }
9+
10+
/**
11+
* @param lang.ast.nodes.TypeDeclaration $type
12+
* @param lang.ast.Result $result
13+
*/
14+
public function __construct($type, $result) {
15+
$this->type= $type;
16+
$this->result= $result;
17+
}
18+
19+
/** @return string */
20+
public function name() { return ltrim($this->type->name, '\\'); }
21+
22+
/**
23+
* Returns whether a given member is an enum case
24+
*
25+
* @param string $member
26+
* @return bool
27+
*/
28+
public function rewriteEnumCase($member) {
29+
if (!self::$ENUMS && 'enum' === $this->type->kind) {
30+
return ($this->type->body[$member] ?? null) instanceof EnumCase;
31+
} else if ('class' === $this->type->kind && '\\lang\\Enum' === $this->type->parent) {
32+
return ($this->type->body['$'.$member] ?? null) instanceof Property;
33+
}
34+
return false;
35+
}
36+
}

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

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,26 @@ protected function declaration($name) {
4040
* - Binary expression where left- and right hand side are literals
4141
*
4242
* @see https://wiki.php.net/rfc/const_scalar_exprs
43+
* @param lang.ast.Result $result
4344
* @param lang.ast.Node $node
4445
* @return bool
4546
*/
46-
protected function isConstant($node) {
47+
protected function isConstant($result, $node) {
4748
if ($node instanceof Literal) {
4849
return true;
4950
} else if ($node instanceof ArrayLiteral) {
5051
foreach ($node->values as $node) {
51-
if (!$this->isConstant($node)) return false;
52+
if (!$this->isConstant($result, $node)) return false;
5253
}
5354
return true;
5455
} else if ($node instanceof ScopeExpression) {
55-
return $node->member instanceof Literal;
56+
return (
57+
$node->member instanceof Literal &&
58+
is_string($node->type) &&
59+
!$result->lookup($node->type)->rewriteEnumCase($node->member->expression)
60+
);
5661
} else if ($node instanceof BinaryExpression) {
57-
return $this->isConstant($node->left) && $this->isConstant($node->right);
62+
return $this->isConstant($result, $node->left) && $this->isConstant($result, $node->right);
5863
}
5964
return false;
6065
}
@@ -188,7 +193,7 @@ protected function emitStatic($result, $static) {
188193
foreach ($static->initializations as $variable => $initial) {
189194
$result->out->write('static $'.$variable);
190195
if ($initial) {
191-
if ($this->isConstant($initial)) {
196+
if ($this->isConstant($result, $initial)) {
192197
$result->out->write('=');
193198
$this->emitOne($result, $initial);
194199
} else {
@@ -284,7 +289,7 @@ protected function emitParameter($result, $parameter) {
284289
$result->out->write(($parameter->reference ? '&' : '').'$'.$parameter->name);
285290
}
286291
if ($parameter->default) {
287-
if ($this->isConstant($parameter->default)) {
292+
if ($this->isConstant($result, $parameter->default)) {
288293
$result->out->write('=');
289294
$this->emitOne($result, $parameter->default);
290295
} else {
@@ -349,7 +354,76 @@ protected function emitLambda($result, $lambda) {
349354
$this->emitOne($result, $lambda->body);
350355
}
351356

357+
protected function emitEnumCase($result, $case) {
358+
$result->out->write('public static $'.$case->name.';');
359+
}
360+
361+
protected function emitEnum($result, $enum) {
362+
array_unshift($result->type, $enum);
363+
array_unshift($result->meta, []);
364+
$result->locals= [[], []];
365+
366+
$result->out->write('final class '.$this->declaration($enum->name).' implements \\'.($enum->base ? 'BackedEnum' : 'UnitEnum'));
367+
$enum->implements && $result->out->write(', '.implode(', ', $enum->implements));
368+
$result->out->write('{');
369+
370+
$cases= [];
371+
foreach ($enum->body as $member) {
372+
if ($member->is('enumcase')) $cases[]= $member;
373+
$this->emitOne($result, $member);
374+
}
375+
376+
// Constructors
377+
if ($enum->base) {
378+
$result->out->write('public $name, $value;');
379+
$result->out->write('private static $values= [];');
380+
$result->out->write('private function __construct($name, $value) {
381+
$this->name= $name;
382+
$this->value= $value;
383+
self::$values[$value]= $this;
384+
}');
385+
$result->out->write('public static function tryFrom($value) {
386+
return self::$values[$value] ?? null;
387+
}');
388+
$result->out->write('public static function from($value) {
389+
if ($r= self::$values[$value] ?? null) return $r;
390+
throw new \Error(\util\Objects::stringOf($value)." is not a valid backing value for enum \"".self::class."\"");
391+
}');
392+
} else {
393+
$result->out->write('public $name;');
394+
$result->out->write('private function __construct($name) {
395+
$this->name= $name;
396+
}');
397+
}
398+
399+
// Enum cases
400+
$result->out->write('public static function cases() { return [');
401+
foreach ($cases as $case) {
402+
$result->out->write('self::$'.$case->name.', ');
403+
}
404+
$result->out->write(']; }');
405+
406+
// Initializations
407+
$result->out->write('static function __init() {');
408+
if ($enum->base) {
409+
foreach ($cases as $case) {
410+
$result->out->write('self::$'.$case->name.'= new self("'.$case->name.'", ');
411+
$this->emitOne($result, $case->expression);
412+
$result->out->write(');');
413+
}
414+
} else {
415+
foreach ($cases as $case) {
416+
$result->out->write('self::$'.$case->name.'= new self("'.$case->name.'");');
417+
}
418+
}
419+
$this->emitInitializations($result, $result->locals[0]);
420+
$this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment);
421+
$result->out->write('}} '.$enum->name.'::__init();');
422+
array_shift($result->type);
423+
}
424+
352425
protected function emitClass($result, $class) {
426+
array_unshift($result->type, $class);
353427
array_unshift($result->meta, []);
354428
$result->locals= [[], []];
355429

@@ -373,6 +447,7 @@ protected function emitClass($result, $class) {
373447
$this->emitInitializations($result, $result->locals[0]);
374448
$this->emitMeta($result, $class->name, $class->annotations, $class->comment);
375449
$result->out->write('}} '.$class->name.'::__init();');
450+
array_shift($result->type);
376451
}
377452

378453
/** Stores lowercased, unnamespaced name in annotations for BC reasons! */
@@ -516,7 +591,7 @@ protected function emitProperty($result, $property) {
516591

517592
$result->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name);
518593
if (isset($property->expression)) {
519-
if ($this->isConstant($property->expression)) {
594+
if ($this->isConstant($result, $property->expression)) {
520595
$result->out->write('=');
521596
$this->emitOne($result, $property->expression);
522597
} else if (in_array('static', $property->modifiers)) {
@@ -569,7 +644,7 @@ protected function emitMethod($result, $method) {
569644
];
570645
}
571646

572-
if (isset($param->default) && !$this->isConstant($param->default)) {
647+
if (isset($param->default) && !$this->isConstant($result, $param->default)) {
573648
$meta[DETAIL_TARGET_ANNO][$param->name]['default']= [$param->default];
574649
}
575650
}
@@ -909,6 +984,8 @@ protected function emitScope($result, $scope) {
909984
$result->out->write(')?'.$t.'::');
910985
$this->emitOne($result, $scope->member);
911986
$result->out->write(':null');
987+
} else if ($scope->member instanceof Literal && $result->lookup($scope->type)->rewriteEnumCase($scope->member->expression)) {
988+
$result->out->write($scope->type.'::$'.$scope->member->expression);
912989
} else {
913990
$result->out->write($scope->type.'::');
914991
$this->emitOne($result, $scope->member);

0 commit comments

Comments
 (0)