Skip to content

Compile PHP enums to PHP 7/8 lookalikes, PHP 8.1 native #106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Mar 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c0a0b71
Use named arguments for annotations
thekid Mar 7, 2021
15f343d
Simplify no_longer_supports_hacklang_variant() test
thekid Mar 7, 2021
6ed6c1c
Initial proof of concept
thekid Mar 7, 2021
42c8060
Add emitter for PHP enums
thekid Mar 8, 2021
b1afdae
Test PHP 8.1 enum lookalikes in conjunction with Enum::valuesOf() / E…
thekid Mar 10, 2021
327a40e
Add missing lang.ast.types package
thekid Mar 10, 2021
94ed624
Require XP core 10.8.0+
thekid Mar 10, 2021
a528053
Add support for backed enums (`class SortOrder: string { ... }`)
thekid Mar 13, 2021
179b15b
Adjust error message to match PHP 8.1 implementation
thekid Mar 13, 2021
b12e5c1
Emit PHP 8.1 native enums if support is available
thekid Mar 13, 2021
71379f9
Make compatible with PHP < 7.4
thekid Mar 13, 2021
ce24ceb
Verify int-backed enums
thekid Mar 13, 2021
0503bde
Use \Error inside ::from()
thekid Mar 13, 2021
aca3aad
Move native enum support detection to types
thekid Mar 13, 2021
24f95be
Move types to lang.ast.emit package
thekid Mar 13, 2021
682e96d
Use lang.Enum directly
thekid Mar 13, 2021
1678c20
Rephrase TODO comment
thekid Mar 13, 2021
54e0997
Invoke from() directly
thekid Mar 13, 2021
80a531e
Verify XPClass::isEnum()
thekid Mar 13, 2021
c929bf9
Verify instance methods work on enums
thekid Mar 13, 2021
68b783d
Verify enums can implement interfaces
thekid Mar 13, 2021
80958e6
Add TODO comments
thekid Mar 13, 2021
e83972a
Verify enums and enum cases can be annotated
thekid Mar 13, 2021
2fc6a5b
Verify cases() for backed enums
thekid Mar 13, 2021
786299f
Restore compatibility with XP < 10.8.0
thekid Mar 13, 2021
d4476da
Use release version for xp-framework/ast
thekid Mar 13, 2021
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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"keywords": ["module", "xp"],
"require" : {
"xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0",
"xp-framework/ast": "^7.0",
"xp-framework/ast": "^7.1",
"php" : ">=7.0.0"
},
"require-dev" : {
Expand Down
19 changes: 19 additions & 0 deletions src/main/php/lang/ast/Result.class.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
<?php namespace lang\ast;

use lang\ast\emit\{Declaration, Reflection};

class Result {
public $out;
public $codegen;
public $line= 1;
public $meta= [];
public $locals= [];
public $stack= [];
public $type= [];

/**
* Starts a result stream, including a preamble
Expand All @@ -28,4 +31,20 @@ public function __construct($out, $preamble= '<?php ') {
public function temp() {
return '$'.$this->codegen->symbol();
}

/**
* Looks up a given type
*
* @param string $type
* @return lang.ast.emit.Type
*/
public function lookup($type) {
if ('self' === $type || 'static' === $type || $type === $this->type[0]->name) {
return new Declaration($this->type[0], $this);
} else if ('parent' === $type) {
return $this->lookup($this->type[0]->parent);
} else {
return new Reflection($type);
}
}
}
36 changes: 36 additions & 0 deletions src/main/php/lang/ast/emit/Declaration.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php namespace lang\ast\emit;

use lang\ast\nodes\{EnumCase, Property};

class Declaration extends Type {
private $type, $result;

static function __static() { }

/**
* @param lang.ast.nodes.TypeDeclaration $type
* @param lang.ast.Result $result
*/
public function __construct($type, $result) {
$this->type= $type;
$this->result= $result;
}

/** @return string */
public function name() { return ltrim($this->type->name, '\\'); }

/**
* Returns whether a given member is an enum case
*
* @param string $member
* @return bool
*/
public function rewriteEnumCase($member) {
if (!self::$ENUMS && 'enum' === $this->type->kind) {
return ($this->type->body[$member] ?? null) instanceof EnumCase;
} else if ('class' === $this->type->kind && '\\lang\\Enum' === $this->type->parent) {
return ($this->type->body['$'.$member] ?? null) instanceof Property;
}
return false;
}
}
93 changes: 85 additions & 8 deletions src/main/php/lang/ast/emit/PHP.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,26 @@ protected function declaration($name) {
* - Binary expression where left- and right hand side are literals
*
* @see https://wiki.php.net/rfc/const_scalar_exprs
* @param lang.ast.Result $result
* @param lang.ast.Node $node
* @return bool
*/
protected function isConstant($node) {
protected function isConstant($result, $node) {
if ($node instanceof Literal) {
return true;
} else if ($node instanceof ArrayLiteral) {
foreach ($node->values as $node) {
if (!$this->isConstant($node)) return false;
if (!$this->isConstant($result, $node)) return false;
}
return true;
} else if ($node instanceof ScopeExpression) {
return $node->member instanceof Literal;
return (
$node->member instanceof Literal &&
is_string($node->type) &&
!$result->lookup($node->type)->rewriteEnumCase($node->member->expression)
);
} else if ($node instanceof BinaryExpression) {
return $this->isConstant($node->left) && $this->isConstant($node->right);
return $this->isConstant($result, $node->left) && $this->isConstant($result, $node->right);
}
return false;
}
Expand Down Expand Up @@ -188,7 +193,7 @@ protected function emitStatic($result, $static) {
foreach ($static->initializations as $variable => $initial) {
$result->out->write('static $'.$variable);
if ($initial) {
if ($this->isConstant($initial)) {
if ($this->isConstant($result, $initial)) {
$result->out->write('=');
$this->emitOne($result, $initial);
} else {
Expand Down Expand Up @@ -284,7 +289,7 @@ protected function emitParameter($result, $parameter) {
$result->out->write(($parameter->reference ? '&' : '').'$'.$parameter->name);
}
if ($parameter->default) {
if ($this->isConstant($parameter->default)) {
if ($this->isConstant($result, $parameter->default)) {
$result->out->write('=');
$this->emitOne($result, $parameter->default);
} else {
Expand Down Expand Up @@ -349,7 +354,76 @@ protected function emitLambda($result, $lambda) {
$this->emitOne($result, $lambda->body);
}

protected function emitEnumCase($result, $case) {
$result->out->write('public static $'.$case->name.';');
}

protected function emitEnum($result, $enum) {
array_unshift($result->type, $enum);
array_unshift($result->meta, []);
$result->locals= [[], []];

$result->out->write('final class '.$this->declaration($enum->name).' implements \\'.($enum->base ? 'BackedEnum' : 'UnitEnum'));
$enum->implements && $result->out->write(', '.implode(', ', $enum->implements));
$result->out->write('{');

$cases= [];
foreach ($enum->body as $member) {
if ($member->is('enumcase')) $cases[]= $member;
$this->emitOne($result, $member);
}

// Constructors
if ($enum->base) {
$result->out->write('public $name, $value;');
$result->out->write('private static $values= [];');
$result->out->write('private function __construct($name, $value) {
$this->name= $name;
$this->value= $value;
self::$values[$value]= $this;
}');
$result->out->write('public static function tryFrom($value) {
return self::$values[$value] ?? null;
}');
$result->out->write('public static function from($value) {
if ($r= self::$values[$value] ?? null) return $r;
throw new \Error(\util\Objects::stringOf($value)." is not a valid backing value for enum \"".self::class."\"");
}');
} else {
$result->out->write('public $name;');
$result->out->write('private function __construct($name) {
$this->name= $name;
}');
}

// Enum cases
$result->out->write('public static function cases() { return [');
foreach ($cases as $case) {
$result->out->write('self::$'.$case->name.', ');
}
$result->out->write(']; }');

// Initializations
$result->out->write('static function __init() {');
if ($enum->base) {
foreach ($cases as $case) {
$result->out->write('self::$'.$case->name.'= new self("'.$case->name.'", ');
$this->emitOne($result, $case->expression);
$result->out->write(');');
}
} else {
foreach ($cases as $case) {
$result->out->write('self::$'.$case->name.'= new self("'.$case->name.'");');
}
}
$this->emitInitializations($result, $result->locals[0]);
$this->emitMeta($result, $enum->name, $enum->annotations, $enum->comment);
$result->out->write('}} '.$enum->name.'::__init();');
array_shift($result->type);
}

protected function emitClass($result, $class) {
array_unshift($result->type, $class);
array_unshift($result->meta, []);
$result->locals= [[], []];

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

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

$result->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name);
if (isset($property->expression)) {
if ($this->isConstant($property->expression)) {
if ($this->isConstant($result, $property->expression)) {
$result->out->write('=');
$this->emitOne($result, $property->expression);
} else if (in_array('static', $property->modifiers)) {
Expand Down Expand Up @@ -569,7 +644,7 @@ protected function emitMethod($result, $method) {
];
}

if (isset($param->default) && !$this->isConstant($param->default)) {
if (isset($param->default) && !$this->isConstant($result, $param->default)) {
$meta[DETAIL_TARGET_ANNO][$param->name]['default']= [$param->default];
}
}
Expand Down Expand Up @@ -909,6 +984,8 @@ protected function emitScope($result, $scope) {
$result->out->write(')?'.$t.'::');
$this->emitOne($result, $scope->member);
$result->out->write(':null');
} else if ($scope->member instanceof Literal && $result->lookup($scope->type)->rewriteEnumCase($scope->member->expression)) {
$result->out->write($scope->type.'::$'.$scope->member->expression);
} else {
$result->out->write($scope->type.'::');
$this->emitOne($result, $scope->member);
Expand Down
Loading