From c0a0b71e24f8f2a1de2c1997b46ecfa0fd785518 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 7 Mar 2021 09:36:42 +0100 Subject: [PATCH 01/26] Use named arguments for annotations --- .../lang/ast/unittest/emit/ArgumentPromotionTest.class.php | 2 +- .../lang/ast/unittest/emit/ControlStructuresTest.class.php | 4 ++-- .../ast/unittest/loader/CompilingClassLoaderTest.class.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php index 96c616da..7c115094 100755 --- a/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ArgumentPromotionTest.class.php @@ -83,7 +83,7 @@ public function __construct(private int $id, private string $name) { } ); } - #[Test, Expect(['class' => Errors::class, 'withMessage' => 'Variadic parameters cannot be promoted'])] + #[Test, Expect(class: Errors::class, withMessage: 'Variadic parameters cannot be promoted')] public function variadic_parameters_cannot_be_promoted() { $this->type('class { public function __construct(private string... $in) { } diff --git a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php index e803f0f2..fbd76d23 100755 --- a/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ControlStructuresTest.class.php @@ -136,7 +136,7 @@ public function run($arg) { Assert::equals('10+ items', $r); } - #[Test, Expect(['class' => Throwable::class, 'withMessage' => '/Unhandled match value of type .+/'])] + #[Test, Expect(class: Throwable::class, withMessage: '/Unhandled match value of type .+/')] public function unhandled_match() { $this->run('class { public function run($arg) { @@ -149,7 +149,7 @@ public function run($arg) { }', SEEK_END); } - #[Test, Expect(['class' => Throwable::class, 'withMessage' => '/Unknown seek mode .+/'])] + #[Test, Expect(class: Throwable::class, withMessage: '/Unknown seek mode .+/')] public function match_with_throw_expression() { $this->run('class { public function run($arg) { diff --git a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php index af1613a8..0cde12f5 100755 --- a/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php +++ b/src/test/php/lang/ast/unittest/loader/CompilingClassLoaderTest.class.php @@ -101,7 +101,7 @@ public function load_uri() { Assert::equals('Tests', $class->getSimpleName()); } - #[Test, Expect(['class' => ClassFormatException::class, 'withMessage' => 'Compiler error: Expected "{", have "(end)"'])] + #[Test, Expect(class: ClassFormatException::class, withMessage: 'Compiler error: Expected "{", have "(end)"')] public function load_class_with_syntax_errors() { $this->compile(['Errors' => "loadClass($types['Errors']); }); } From 15f343daaa116d4a9d8eb85eb80bf4bc88f1679a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 7 Mar 2021 09:38:57 +0100 Subject: [PATCH 02/26] Simplify no_longer_supports_hacklang_variant() test --- .../ast/unittest/emit/LambdasTest.class.php | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php index f10fb253..4db484e8 100755 --- a/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/LambdasTest.class.php @@ -178,17 +178,12 @@ public function run() { Assert::equals(2, $r()); } - #[Test] + #[Test, Expect(Errors::class)] public function no_longer_supports_hacklang_variant() { - try { - $this->run('class { - public function run() { - $func= ($arg) ==> { return 1; }; - } - }'); - $this->fail('No errors raised', null, Errors::class); - } catch (Errors $expected) { - \xp::gc(); - } + $this->run('class { + public function run() { + $func= ($arg) ==> { return 1; }; + } + }'); } } \ No newline at end of file From 6ed6c1c89b66dd1a784b802c4fcfd47e5bfd83a2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 7 Mar 2021 14:25:48 +0100 Subject: [PATCH 03/26] Initial proof of concept --- src/main/php/lang/ast/Result.class.php | 19 +++++++++++++++++++ src/main/php/lang/ast/emit/PHP.class.php | 4 ++++ .../ast/unittest/emit/MembersTest.class.php | 13 +++++++++++++ 3 files changed, 36 insertions(+) diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index 166d192c..6102986d 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -1,5 +1,7 @@ codegen->symbol(); } + + /** + * Looks up a given type + * + * @param string $type + * @return lang.ast.types.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); + } + } } \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 7afbf52b..1c89b771 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -350,6 +350,7 @@ protected function emitLambda($result, $lambda) { } protected function emitClass($result, $class) { + array_unshift($result->type, $class); array_unshift($result->meta, []); $result->locals= [[], []]; @@ -373,6 +374,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! */ @@ -909,6 +911,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 && '$enum' === $result->lookup($scope->type)->kind()) { + $result->out->write($scope->type.'::$'.$scope->member->expression); } else { $result->out->write($scope->type.'::'); $this->emitOne($result, $scope->member); diff --git a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php index 6807e5ef..beca43f1 100755 --- a/src/test/php/lang/ast/unittest/emit/MembersTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/MembersTest.class.php @@ -171,6 +171,19 @@ public function run() { Assert::equals('MON', $r); } + #[Test] + public function allow_constant_syntax_for_members() { + $r= $this->run('use lang\{Enum, CommandLine}; class extends Enum { + public static $MON, $TUE, $WED, $THU, $FRI, $SAT, $SUN; + + public function run() { + return [self::MON->name(), ::TUE->name(), CommandLine::WINDOWS->name()]; + } + }'); + + Assert::equals(['MON', 'TUE', 'WINDOWS'], $r); + } + #[Test] public function method_with_static() { $r= $this->run('class { From 42c806048876175c577fd5dd85fb370cf621ccf5 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 8 Mar 2021 20:06:22 +0100 Subject: [PATCH 04/26] Add emitter for PHP enums See xp-framework/ast#23 --- composer.json | 2 +- src/main/php/lang/ast/emit/PHP.class.php | 63 ++++++++++++++++--- .../lang/ast/unittest/emit/EnumTest.class.php | 53 ++++++++++++++++ 3 files changed, 108 insertions(+), 10 deletions(-) create mode 100755 src/test/php/lang/ast/unittest/emit/EnumTest.class.php diff --git a/composer.json b/composer.json index 66cf5680..d4497400 100755 --- a/composer.json +++ b/composer.json @@ -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": "dev-feature/php-enums as 7.1.0", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 1c89b771..5d0f4a85 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -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) && + 'enum' !== $result->lookup($node->type)->kind() + ); } 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; } @@ -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 { @@ -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 { @@ -349,6 +354,46 @@ 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 \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); + } + + // Name and constructor + $result->out->write('public $name; 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() {'); + 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, []); @@ -518,7 +563,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)) { @@ -571,7 +616,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]; } } @@ -911,7 +956,7 @@ protected function emitScope($result, $scope) { $result->out->write(')?'.$t.'::'); $this->emitOne($result, $scope->member); $result->out->write(':null'); - } else if ($scope->member instanceof Literal && '$enum' === $result->lookup($scope->type)->kind()) { + } else if ($scope->member instanceof Literal && 'enum' === $result->lookup($scope->type)->kind()) { $result->out->write($scope->type.'::$'.$scope->member->expression); } else { $result->out->write($scope->type.'::'); diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php new file mode 100755 index 00000000..893e8f62 --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -0,0 +1,53 @@ + function_exists("enum_exists"))')] +class EnumTest extends EmittingTest { + + #[Test] + public function name_property() { + $t= $this->type('enum { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + + public static function run() { + return self::Hearts->name; + } + }'); + + Assert::equals('Hearts', $t->getMethod('run')->invoke(null)); + } + + #[Test] + public function cases_method() { + $t= $this->type('enum { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + }'); + + Assert::equals( + ['Hearts', 'Diamonds', 'Clubs', 'Spades'], + array_map(function($suit) { return $suit->name; }, $t->getMethod('cases')->invoke(null)) + ); + } + + #[Test] + public function used_as_parameter_default() { + $t= $this->type('enum { + case ASC; + case DESC; + + public static function run($order= self::ASC) { + return $order->name; + } + }'); + + Assert::equals('ASC', $t->getMethod('run')->invoke(null)); + } +} \ No newline at end of file From b1afdaec295768c46ead140b087d3e941a363725 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 10 Mar 2021 20:05:34 +0100 Subject: [PATCH 05/26] Test PHP 8.1 enum lookalikes in conjunction with Enum::valuesOf() / Enum::valueOf() --- src/main/php/lang/ast/emit/PHP.class.php | 4 +-- .../lang/ast/unittest/emit/EnumTest.class.php | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 5d0f4a85..5dd4fd49 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -56,7 +56,7 @@ protected function isConstant($result, $node) { return ( $node->member instanceof Literal && is_string($node->type) && - 'enum' !== $result->lookup($node->type)->kind() + !$result->lookup($node->type)->isEnumCase($node->member->expression) ); } else if ($node instanceof BinaryExpression) { return $this->isConstant($result, $node->left) && $this->isConstant($result, $node->right); @@ -956,7 +956,7 @@ protected function emitScope($result, $scope) { $result->out->write(')?'.$t.'::'); $this->emitOne($result, $scope->member); $result->out->write(':null'); - } else if ($scope->member instanceof Literal && 'enum' === $result->lookup($scope->type)->kind()) { + } else if ($scope->member instanceof Literal && $result->lookup($scope->type)->isEnumCase($scope->member->expression)) { $result->out->write($scope->type.'::$'.$scope->member->expression); } else { $result->out->write($scope->type.'::'); diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 893e8f62..ad758521 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -50,4 +50,39 @@ public static function run($order= self::ASC) { Assert::equals('ASC', $t->getMethod('run')->invoke(null)); } + + #[Test] + public function enum_values() { + $t= $this->type('use lang\Enum; enum { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + + public static function run() { + return Enum::valuesOf(self::class); + } + }'); + + Assert::equals( + ['Hearts', 'Diamonds', 'Clubs', 'Spades'], + array_map(function($suit) { return $suit->name; }, $t->getMethod('run')->invoke(null)) + ); + } + + #[Test] + public function enum_value() { + $t= $this->type('use lang\Enum; enum { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + + public static function run() { + return Enum::valueOf(self::class, "Hearts")->name; + } + }'); + + Assert::equals('Hearts', $t->getMethod('run')->invoke(null)); + } } \ No newline at end of file From 327a40e9b3a13217195472a678a26f425c8affee Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Wed, 10 Mar 2021 20:23:43 +0100 Subject: [PATCH 06/26] Add missing lang.ast.types package --- .../php/lang/ast/types/Declaration.class.php | 34 ++++++++++++++++++ .../php/lang/ast/types/Reflection.class.php | 35 +++++++++++++++++++ src/main/php/lang/ast/types/Type.class.php | 15 ++++++++ 3 files changed, 84 insertions(+) create mode 100755 src/main/php/lang/ast/types/Declaration.class.php create mode 100755 src/main/php/lang/ast/types/Reflection.class.php create mode 100755 src/main/php/lang/ast/types/Type.class.php diff --git a/src/main/php/lang/ast/types/Declaration.class.php b/src/main/php/lang/ast/types/Declaration.class.php new file mode 100755 index 00000000..09dfc9bc --- /dev/null +++ b/src/main/php/lang/ast/types/Declaration.class.php @@ -0,0 +1,34 @@ +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 isEnumCase($member) { + if ('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; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/types/Reflection.class.php b/src/main/php/lang/ast/types/Reflection.class.php new file mode 100755 index 00000000..ebc12b84 --- /dev/null +++ b/src/main/php/lang/ast/types/Reflection.class.php @@ -0,0 +1,35 @@ +reflect= new \ReflectionClass($type); + } catch (\ReflectionException $e) { + throw new ClassNotFoundException($type); + } + } + + /** @return string */ + public function name() { return $this->reflect->name; } + + /** + * Returns whether a given member is an enum case + * + * @param string $member + * @return bool + */ + public function isEnumCase($member) { + if ($this->reflect->isSubclassOf(Enum::class)) { + return $this->reflect->getStaticPropertyValue($member, null) instanceof Enum; + } else if ($this->reflect->isSubclassOf(\UnitEnum::class)) { + $value= $this->reflect->getConstant($member) ?: $this->reflect->getStaticPropertyValue($member, null); + return $value instanceof \UnitEnum; + } + return false; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/types/Type.class.php b/src/main/php/lang/ast/types/Type.class.php new file mode 100755 index 00000000..d9e23b3e --- /dev/null +++ b/src/main/php/lang/ast/types/Type.class.php @@ -0,0 +1,15 @@ + Date: Wed, 10 Mar 2021 20:30:10 +0100 Subject: [PATCH 07/26] Require XP core 10.8.0+ --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d4497400..7d86f528 100755 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "description" : "XP Compiler", "keywords": ["module", "xp"], "require" : { - "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", + "xp-framework/core": "^10.8", "xp-framework/ast": "dev-feature/php-enums as 7.1.0", "php" : ">=7.0.0" }, From a528053b5505b7531a30b06609ad66bc5dd96baa Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 10:11:55 +0100 Subject: [PATCH 08/26] Add support for backed enums (`class SortOrder: string { ... }`) --- src/main/php/lang/ast/emit/PHP.class.php | 38 ++++++++++-- .../lang/ast/unittest/emit/EnumTest.class.php | 61 +++++++++++++++++++ 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 5dd4fd49..f1af5edb 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -363,7 +363,7 @@ protected function emitEnum($result, $enum) { array_unshift($result->meta, []); $result->locals= [[], []]; - $result->out->write('final class '.$this->declaration($enum->name).' implements \UnitEnum'); + $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('{'); @@ -373,8 +373,28 @@ protected function emitEnum($result, $enum) { $this->emitOne($result, $member); } - // Name and constructor - $result->out->write('public $name; private function __construct($name) { $this->name= $name; }'); + // 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 \ValueError("Not an enum value: ".\util\Objects::stringOf($value)); + }'); + } 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 ['); @@ -385,8 +405,16 @@ protected function emitEnum($result, $enum) { // Initializations $result->out->write('static function __init() {'); - foreach ($cases as $case) { - $result->out->write('self::$'.$case->name.'= new self("'.$case->name.'");'); + 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); diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index ad758521..9855a32e 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -51,6 +51,67 @@ public static function run($order= self::ASC) { Assert::equals('ASC', $t->getMethod('run')->invoke(null)); } + #[Test] + public function value_property_of_backed_enum() { + $t= $this->type('enum : string { + case ASC = "asc"; + case DESC = "desc"; + + public static function run() { + return self::DESC->value; + } + }'); + + Assert::equals('desc', $t->getMethod('run')->invoke(null)); + } + + #[Test, Values([['asc', 'ASC'], ['desc', 'DESC']])] + public function backed_enum_from($arg, $expected) { + $t= $this->type('enum : string { + case ASC = "asc"; + case DESC = "desc"; + + public static function run($arg) { + return self::from($arg)->name; + } + }'); + + Assert::equals($expected, $t->getMethod('run')->invoke(null, [$arg])); + } + + #[Test] + public function backed_enum_from_nonexistant() { + $t= $this->type('enum : string { + case ASC = "asc"; + case DESC = "desc"; + + public static function run() { + try { + self::from("illegal"); + throw new \lang\IllegalStateException("No exception raised"); + } catch (\ValueError $expected) { + return $expected->getMessage(); + } + } + }'); + + Assert::equals('Not an enum value: "illegal"', $t->getMethod('run')->invoke(null)); + } + + #[Test, Values([['asc', 'ASC'], ['desc', 'DESC'], ['illegal', null]])] + public function backed_enum_tryFrom($arg, $expected) { + $t= $this->type('enum : string { + case ASC = "asc"; + case DESC = "desc"; + + public static function run($arg) { + return self::tryFrom($arg)?->name; + } + }'); + + Assert::equals($expected, $t->getMethod('run')->invoke(null, [$arg])); + } + #[Test] public function enum_values() { $t= $this->type('use lang\Enum; enum { From 179b15b89af389a731d1fa328e839bf5d9c61818 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 10:43:24 +0100 Subject: [PATCH 09/26] Adjust error message to match PHP 8.1 implementation --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- src/test/php/lang/ast/unittest/emit/EnumTest.class.php | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index f1af5edb..2abad2bc 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -387,7 +387,7 @@ protected function emitEnum($result, $enum) { }'); $result->out->write('public static function from($value) { if ($r= self::$values[$value] ?? null) return $r; - throw new \ValueError("Not an enum value: ".\util\Objects::stringOf($value)); + throw new \ValueError(\util\Objects::stringOf($value)." is not a valid backing value for enum \"".self::class."\""); }'); } else { $result->out->write('public $name;'); diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 9855a32e..7c19eeaa 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -81,21 +81,24 @@ public static function run($arg) { #[Test] public function backed_enum_from_nonexistant() { - $t= $this->type('enum : string { + $t= $this->type('use lang\IllegalStateException; enum : string { case ASC = "asc"; case DESC = "desc"; public static function run() { try { self::from("illegal"); - throw new \lang\IllegalStateException("No exception raised"); + throw new IllegalStateException("No exception raised"); } catch (\ValueError $expected) { return $expected->getMessage(); } } }'); - Assert::equals('Not an enum value: "illegal"', $t->getMethod('run')->invoke(null)); + Assert::equals( + '"illegal" is not a valid backing value for enum "'.$t->literal().'"', + $t->getMethod('run')->invoke(null) + ); } #[Test, Values([['asc', 'ASC'], ['desc', 'DESC'], ['illegal', null]])] From b12e5c15df2a6ca27552346da9547c2608143bcf Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:08:02 +0100 Subject: [PATCH 10/26] Emit PHP 8.1 native enums if support is available --- src/main/php/lang/ast/emit/PHP.class.php | 4 +- src/main/php/lang/ast/emit/PHP81.class.php | 212 ++++++++++++++++++ .../php/lang/ast/types/Declaration.class.php | 5 +- .../php/lang/ast/types/Reflection.class.php | 5 +- src/main/php/lang/ast/types/Type.class.php | 3 +- 5 files changed, 222 insertions(+), 7 deletions(-) create mode 100755 src/main/php/lang/ast/emit/PHP81.class.php diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 2abad2bc..9d786370 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -56,7 +56,7 @@ protected function isConstant($result, $node) { return ( $node->member instanceof Literal && is_string($node->type) && - !$result->lookup($node->type)->isEnumCase($node->member->expression) + !$result->lookup($node->type)->rewriteEnumCase($node->member->expression) ); } else if ($node instanceof BinaryExpression) { return $this->isConstant($result, $node->left) && $this->isConstant($result, $node->right); @@ -984,7 +984,7 @@ 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)->isEnumCase($scope->member->expression)) { + } 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.'::'); diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php new file mode 100755 index 00000000..9b9af638 --- /dev/null +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -0,0 +1,212 @@ + literal mappings */ + public function __construct() { + $this->literals= [ + IsArray::class => function($t) { return 'array'; }, + IsMap::class => function($t) { return 'array'; }, + IsFunction::class => function($t) { return 'callable'; }, + IsValue::class => function($t) { return $t->literal(); }, + IsNullable::class => function($t) { $l= $this->literal($t->element); return null === $l ? null : '?'.$l; }, + IsUnion::class => function($t) { + $u= ''; + foreach ($t->components as $component) { + if (null === ($l= $this->literal($component))) return null; + $u.= '|'.$l; + } + return substr($u, 1); + }, + IsLiteral::class => function($t) { return $t->literal(); } + ]; + } + + /** + * Returns whether a given node is a constant expression: + * + * - Any literal + * - Arrays where all members are literals + * - Scope expressions with literal members (self::class, T::const) + * - 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($result, $node) { + if ($node instanceof Literal) { + return true; + } else if ($node instanceof ArrayLiteral) { + foreach ($node->values as $node) { + if (!$this->isConstant($result, $node)) return false; + } + return true; + } else if ($node instanceof ScopeExpression) { + return ( + $node->member instanceof Literal && + is_string($node->type) && + !$result->lookup($node->type)->rewriteEnumCase($node->member->expression, self::$ENUMS) + ); + } else if ($node instanceof BinaryExpression) { + return $this->isConstant($result, $node->left) && $this->isConstant($result, $node->right); + } + return false; + } + + protected function emitArguments($result, $arguments) { + $i= 0; + foreach ($arguments as $name => $argument) { + if ($i++) $result->out->write(','); + if (is_string($name)) $result->out->write($name.':'); + $this->emitOne($result, $argument); + } + } + + protected function emitScope($result, $scope) { + if ($scope->type instanceof Variable) { + $this->emitOne($result, $scope->type); + $result->out->write('::'); + $this->emitOne($result, $scope->member); + } else if ($scope->type instanceof Node) { + $t= $result->temp(); + $result->out->write('('.$t.'='); + $this->emitOne($result, $scope->type); + $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, self::$ENUMS)) { + $result->out->write($scope->type.'::$'.$scope->member->expression); + } else { + $result->out->write($scope->type.'::'); + $this->emitOne($result, $scope->member); + } + } + + protected function emitEnumCase($result, $case) { + if (self::$ENUMS) { + $result->out->write('case '.$case->name); + if ($case->expression) { + $result->out->write('='); + $this->emitOne($result, $case->expression); + } + $result->out->write(';'); + } else { + parent::emitEnumCase($result, $case); + } + } + + protected function emitEnum($result, $enum) { + if (self::$ENUMS) { + array_unshift($result->type, $enum); + array_unshift($result->meta, []); + $result->locals= [[], []]; + + $result->out->write('enum '.$this->declaration($enum->name)); + $enum->base && $result->out->write(':'.$enum->base); + $enum->implements && $result->out->write(' implements '.implode(', ', $enum->implements)); + $result->out->write('{'); + + foreach ($enum->body as $member) { + $this->emitOne($result, $member); + } + + // Initializations + $result->out->write('static function __init() {'); + $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); + } else { + parent::emitEnum($result, $enum); + } + } + + protected function emitNew($result, $new) { + if ($new->type instanceof Node) { + $result->out->write('new ('); + $this->emitOne($result, $new->type); + $result->out->write(')('); + } else { + $result->out->write('new '.$new->type.'('); + } + + $this->emitArguments($result, $new->arguments); + $result->out->write(')'); + } + + protected function emitThrowExpression($result, $throw) { + $result->out->write('throw '); + $this->emitOne($result, $throw->expression); + } + + protected function emitCatch($result, $catch) { + $capture= $catch->variable ? ' $'.$catch->variable : ''; + if (empty($catch->types)) { + $result->out->write('catch(\\Throwable'.$capture.') {'); + } else { + $result->out->write('catch('.implode('|', $catch->types).$capture.') {'); + } + $this->emitAll($result, $catch->body); + $result->out->write('}'); + } + + protected function emitNullsafeInstance($result, $instance) { + $this->emitOne($result, $instance->expression); + $result->out->write('?->'); + + if ('literal' === $instance->member->kind) { + $result->out->write($instance->member->expression); + } else { + $result->out->write('{'); + $this->emitOne($result, $instance->member); + $result->out->write('}'); + } + } + + protected function emitMatch($result, $match) { + if (null === $match->expression) { + $result->out->write('match (true) {'); + } else { + $result->out->write('match ('); + $this->emitOne($result, $match->expression); + $result->out->write(') {'); + } + + foreach ($match->cases as $case) { + $b= 0; + foreach ($case->expressions as $expression) { + $b && $result->out->write(','); + $this->emitOne($result, $expression); + $b++; + } + $result->out->write('=>'); + $this->emitAsExpression($result, $case->body); + $result->out->write(','); + } + + if ($match->default) { + $result->out->write('default=>'); + $this->emitAsExpression($result, $match->default); + } + + $result->out->write('}'); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/types/Declaration.class.php b/src/main/php/lang/ast/types/Declaration.class.php index 09dfc9bc..85b2412e 100755 --- a/src/main/php/lang/ast/types/Declaration.class.php +++ b/src/main/php/lang/ast/types/Declaration.class.php @@ -21,10 +21,11 @@ public function name() { return ltrim($this->type->name, '\\'); } * Returns whether a given member is an enum case * * @param string $member + * @param bool $native Whether native enum support exists * @return bool */ - public function isEnumCase($member) { - if ('enum' === $this->type->kind) { + public function rewriteEnumCase($member, $native= false) { + if (!$native && '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; diff --git a/src/main/php/lang/ast/types/Reflection.class.php b/src/main/php/lang/ast/types/Reflection.class.php index ebc12b84..1fb0e5c3 100755 --- a/src/main/php/lang/ast/types/Reflection.class.php +++ b/src/main/php/lang/ast/types/Reflection.class.php @@ -21,12 +21,13 @@ public function name() { return $this->reflect->name; } * Returns whether a given member is an enum case * * @param string $member + * @param bool $native Whether enums are natively supported * @return bool */ - public function isEnumCase($member) { + public function rewriteEnumCase($member, $native= false) { if ($this->reflect->isSubclassOf(Enum::class)) { return $this->reflect->getStaticPropertyValue($member, null) instanceof Enum; - } else if ($this->reflect->isSubclassOf(\UnitEnum::class)) { + } else if (!$native && $this->reflect->isSubclassOf(\UnitEnum::class)) { $value= $this->reflect->getConstant($member) ?: $this->reflect->getStaticPropertyValue($member, null); return $value instanceof \UnitEnum; } diff --git a/src/main/php/lang/ast/types/Type.class.php b/src/main/php/lang/ast/types/Type.class.php index d9e23b3e..4f092b09 100755 --- a/src/main/php/lang/ast/types/Type.class.php +++ b/src/main/php/lang/ast/types/Type.class.php @@ -9,7 +9,8 @@ public function name(); * Returns whether a given member is an enum case * * @param string $member + * @param bool $native Whether native enum support exists * @return bool */ - public function isEnumCase($member); + public function rewriteEnumCase($member, $native= false); } \ No newline at end of file From 71379f9431263706d80d9edce85f0e0d4016c3b4 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:10:03 +0100 Subject: [PATCH 11/26] Make compatible with PHP < 7.4 --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- src/test/php/lang/ast/unittest/emit/EnumTest.class.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 9d786370..0b9eb7d3 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -387,7 +387,7 @@ protected function emitEnum($result, $enum) { }'); $result->out->write('public static function from($value) { if ($r= self::$values[$value] ?? null) return $r; - throw new \ValueError(\util\Objects::stringOf($value)." is not a valid backing value for enum \"".self::class."\""); + throw new \Exception(\util\Objects::stringOf($value)." is not a valid backing value for enum \"".self::class."\""); }'); } else { $result->out->write('public $name;'); diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 7c19eeaa..6a80f077 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -89,7 +89,7 @@ public static function run() { try { self::from("illegal"); throw new IllegalStateException("No exception raised"); - } catch (\ValueError $expected) { + } catch (\Exception $expected) { return $expected->getMessage(); } } From ce24cebda2b40940434b3dac592350d28a928a77 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:14:13 +0100 Subject: [PATCH 12/26] Verify int-backed enums --- .../lang/ast/unittest/emit/EnumTest.class.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 6a80f077..6efdcaf7 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -65,8 +65,22 @@ public static function run() { Assert::equals('desc', $t->getMethod('run')->invoke(null)); } + #[Test, Values([[0, 'NO'], [1, 'YES']])] + public function backed_enum_from_int($arg, $expected) { + $t= $this->type('enum : int { + case NO = 0; + case YES = 1; + + public static function run($arg) { + return self::from($arg)->name; + } + }'); + + Assert::equals($expected, $t->getMethod('run')->invoke(null, [$arg])); + } + #[Test, Values([['asc', 'ASC'], ['desc', 'DESC']])] - public function backed_enum_from($arg, $expected) { + public function backed_enum_from_string($arg, $expected) { $t= $this->type('enum : string { case ASC = "asc"; case DESC = "desc"; @@ -89,7 +103,7 @@ public static function run() { try { self::from("illegal"); throw new IllegalStateException("No exception raised"); - } catch (\Exception $expected) { + } catch (\Throwable $expected) { return $expected->getMessage(); } } From 0503bde2a65999a8506469592c8232c9ec428704 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:18:35 +0100 Subject: [PATCH 13/26] Use \Error inside ::from() --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- src/test/php/lang/ast/unittest/emit/EnumTest.class.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 0b9eb7d3..1c4198e2 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -387,7 +387,7 @@ protected function emitEnum($result, $enum) { }'); $result->out->write('public static function from($value) { if ($r= self::$values[$value] ?? null) return $r; - throw new \Exception(\util\Objects::stringOf($value)." is not a valid backing value for enum \"".self::class."\""); + throw new \Error(\util\Objects::stringOf($value)." is not a valid backing value for enum \"".self::class."\""); }'); } else { $result->out->write('public $name;'); diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 6efdcaf7..1e9b9123 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -103,7 +103,7 @@ public static function run() { try { self::from("illegal"); throw new IllegalStateException("No exception raised"); - } catch (\Throwable $expected) { + } catch (\Error $expected) { return $expected->getMessage(); } } From aca3aadbbf5478cdee9ae4832b81a833852a7f40 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:25:12 +0100 Subject: [PATCH 14/26] Move native enum support detection to types --- src/main/php/lang/ast/emit/PHP81.class.php | 65 +------------------ .../php/lang/ast/types/Declaration.class.php | 9 +-- .../php/lang/ast/types/Reflection.class.php | 9 +-- src/main/php/lang/ast/types/Type.class.php | 12 ++-- .../lang/ast/unittest/emit/EnumTest.class.php | 17 +++++ 5 files changed, 38 insertions(+), 74 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index 9b9af638..b6586506 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -1,7 +1,7 @@ literal mappings */ public function __construct() { $this->literals= [ @@ -38,39 +32,6 @@ public function __construct() { ]; } - /** - * Returns whether a given node is a constant expression: - * - * - Any literal - * - Arrays where all members are literals - * - Scope expressions with literal members (self::class, T::const) - * - 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($result, $node) { - if ($node instanceof Literal) { - return true; - } else if ($node instanceof ArrayLiteral) { - foreach ($node->values as $node) { - if (!$this->isConstant($result, $node)) return false; - } - return true; - } else if ($node instanceof ScopeExpression) { - return ( - $node->member instanceof Literal && - is_string($node->type) && - !$result->lookup($node->type)->rewriteEnumCase($node->member->expression, self::$ENUMS) - ); - } else if ($node instanceof BinaryExpression) { - return $this->isConstant($result, $node->left) && $this->isConstant($result, $node->right); - } - return false; - } - protected function emitArguments($result, $arguments) { $i= 0; foreach ($arguments as $name => $argument) { @@ -80,28 +41,8 @@ protected function emitArguments($result, $arguments) { } } - protected function emitScope($result, $scope) { - if ($scope->type instanceof Variable) { - $this->emitOne($result, $scope->type); - $result->out->write('::'); - $this->emitOne($result, $scope->member); - } else if ($scope->type instanceof Node) { - $t= $result->temp(); - $result->out->write('('.$t.'='); - $this->emitOne($result, $scope->type); - $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, self::$ENUMS)) { - $result->out->write($scope->type.'::$'.$scope->member->expression); - } else { - $result->out->write($scope->type.'::'); - $this->emitOne($result, $scope->member); - } - } - protected function emitEnumCase($result, $case) { - if (self::$ENUMS) { + if (Type::$ENUMS) { $result->out->write('case '.$case->name); if ($case->expression) { $result->out->write('='); @@ -114,7 +55,7 @@ protected function emitEnumCase($result, $case) { } protected function emitEnum($result, $enum) { - if (self::$ENUMS) { + if (Type::$ENUMS) { array_unshift($result->type, $enum); array_unshift($result->meta, []); $result->locals= [[], []]; diff --git a/src/main/php/lang/ast/types/Declaration.class.php b/src/main/php/lang/ast/types/Declaration.class.php index 85b2412e..954ae39c 100755 --- a/src/main/php/lang/ast/types/Declaration.class.php +++ b/src/main/php/lang/ast/types/Declaration.class.php @@ -2,9 +2,11 @@ use lang\ast\nodes\{EnumCase, Property}; -class Declaration implements Type { +class Declaration extends Type { private $type, $result; + static function __static() { } + /** * @param lang.ast.nodes.TypeDeclaration $type * @param lang.ast.Result $result @@ -21,11 +23,10 @@ public function name() { return ltrim($this->type->name, '\\'); } * Returns whether a given member is an enum case * * @param string $member - * @param bool $native Whether native enum support exists * @return bool */ - public function rewriteEnumCase($member, $native= false) { - if (!$native && 'enum' === $this->type->kind) { + 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; diff --git a/src/main/php/lang/ast/types/Reflection.class.php b/src/main/php/lang/ast/types/Reflection.class.php index 1fb0e5c3..fecc2b1d 100755 --- a/src/main/php/lang/ast/types/Reflection.class.php +++ b/src/main/php/lang/ast/types/Reflection.class.php @@ -2,9 +2,11 @@ use lang\{Enum, ClassNotFoundException}; -class Reflection implements Type { +class Reflection extends Type { private $reflect; + static function __static() { } + /** @param string $type */ public function __construct($type) { try { @@ -21,13 +23,12 @@ public function name() { return $this->reflect->name; } * Returns whether a given member is an enum case * * @param string $member - * @param bool $native Whether enums are natively supported * @return bool */ - public function rewriteEnumCase($member, $native= false) { + public function rewriteEnumCase($member) { if ($this->reflect->isSubclassOf(Enum::class)) { return $this->reflect->getStaticPropertyValue($member, null) instanceof Enum; - } else if (!$native && $this->reflect->isSubclassOf(\UnitEnum::class)) { + } else if (!self::$ENUMS && $this->reflect->isSubclassOf(\UnitEnum::class)) { $value= $this->reflect->getConstant($member) ?: $this->reflect->getStaticPropertyValue($member, null); return $value instanceof \UnitEnum; } diff --git a/src/main/php/lang/ast/types/Type.class.php b/src/main/php/lang/ast/types/Type.class.php index 4f092b09..f2f5b2ba 100755 --- a/src/main/php/lang/ast/types/Type.class.php +++ b/src/main/php/lang/ast/types/Type.class.php @@ -1,16 +1,20 @@ type('enum { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + + const COLORS = ["red", "black"]; + }'); + + Assert::equals( + ['Hearts', 'Diamonds', 'Clubs', 'Spades'], + array_map(function($suit) { return $suit->name; }, $t->getMethod('cases')->invoke(null)) + ); + } + #[Test] public function used_as_parameter_default() { $t= $this->type('enum { From 24f95be10e810ac005c67b9f6b4c5484d5adbc20 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:27:02 +0100 Subject: [PATCH 15/26] Move types to lang.ast.emit package --- src/main/php/lang/ast/Result.class.php | 4 ++-- src/main/php/lang/ast/{types => emit}/Declaration.class.php | 2 +- src/main/php/lang/ast/emit/PHP81.class.php | 1 - src/main/php/lang/ast/{types => emit}/Reflection.class.php | 2 +- src/main/php/lang/ast/{types => emit}/Type.class.php | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) rename src/main/php/lang/ast/{types => emit}/Declaration.class.php (96%) rename src/main/php/lang/ast/{types => emit}/Reflection.class.php (96%) rename src/main/php/lang/ast/{types => emit}/Type.class.php (92%) diff --git a/src/main/php/lang/ast/Result.class.php b/src/main/php/lang/ast/Result.class.php index 6102986d..310d7b74 100755 --- a/src/main/php/lang/ast/Result.class.php +++ b/src/main/php/lang/ast/Result.class.php @@ -1,6 +1,6 @@ type[0]->name) { diff --git a/src/main/php/lang/ast/types/Declaration.class.php b/src/main/php/lang/ast/emit/Declaration.class.php similarity index 96% rename from src/main/php/lang/ast/types/Declaration.class.php rename to src/main/php/lang/ast/emit/Declaration.class.php index 954ae39c..b8384f93 100755 --- a/src/main/php/lang/ast/types/Declaration.class.php +++ b/src/main/php/lang/ast/emit/Declaration.class.php @@ -1,4 +1,4 @@ - Date: Sat, 13 Mar 2021 11:30:46 +0100 Subject: [PATCH 16/26] Use lang.Enum directly --- .../lang/ast/unittest/emit/EnumTest.class.php | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index c945c110..13ddb13b 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -1,5 +1,6 @@ getMethod('run')->invoke(null)); } + #[Test] + public function overwritten_parameter_default_value() { + $t= $this->type('enum { + case ASC; + case DESC; + + public static function run($order= self::ASC) { + return $order->name; + } + }'); + + Assert::equals('DESC', $t->getMethod('run')->invoke(null, [Enum::valueOf($t, 'DESC')])); + } + #[Test] public function value_property_of_backed_enum() { $t= $this->type('enum : string { @@ -148,36 +163,28 @@ public static function run($arg) { #[Test] public function enum_values() { - $t= $this->type('use lang\Enum; enum { + $t= $this->type('enum { case Hearts; case Diamonds; case Clubs; case Spades; - - public static function run() { - return Enum::valuesOf(self::class); - } }'); Assert::equals( ['Hearts', 'Diamonds', 'Clubs', 'Spades'], - array_map(function($suit) { return $suit->name; }, $t->getMethod('run')->invoke(null)) + array_map(function($suit) { return $suit->name; }, Enum::valuesOf($t)) ); } #[Test] public function enum_value() { - $t= $this->type('use lang\Enum; enum { + $t= $this->type('enum { case Hearts; case Diamonds; case Clubs; case Spades; - - public static function run() { - return Enum::valueOf(self::class, "Hearts")->name; - } }'); - Assert::equals('Hearts', $t->getMethod('run')->invoke(null)); + Assert::equals('Hearts', Enum::valueOf($t, 'Hearts')->name); } } \ No newline at end of file From 1678c2079cb586842e12f36d4da72662d99dc157 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:34:31 +0100 Subject: [PATCH 17/26] Rephrase TODO comment --- src/main/php/lang/ast/emit/Type.class.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/Type.class.php b/src/main/php/lang/ast/emit/Type.class.php index 3b383410..211763d7 100755 --- a/src/main/php/lang/ast/emit/Type.class.php +++ b/src/main/php/lang/ast/emit/Type.class.php @@ -4,7 +4,9 @@ abstract class Type { public static $ENUMS; static function __static() { - self::$ENUMS= class_exists(\ReflectionEnum::class, false); // TODO remove once enum PR is merged + + // TODO: Check PHP version ID once enum PR is merged + self::$ENUMS= class_exists(\ReflectionEnum::class, false); } /** @return string */ From 54e0997718d1971fc1704f7c6bea188ecbdd8634 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:36:30 +0100 Subject: [PATCH 18/26] Invoke from() directly --- .../php/lang/ast/unittest/emit/EnumTest.class.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 13ddb13b..ef3c78f1 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -102,13 +102,9 @@ public function backed_enum_from_int($arg, $expected) { $t= $this->type('enum : int { case NO = 0; case YES = 1; - - public static function run($arg) { - return self::from($arg)->name; - } }'); - Assert::equals($expected, $t->getMethod('run')->invoke(null, [$arg])); + Assert::equals($expected, $t->getMethod('from')->invoke(null, [$arg])->name); } #[Test, Values([['asc', 'ASC'], ['desc', 'DESC']])] @@ -116,13 +112,9 @@ public function backed_enum_from_string($arg, $expected) { $t= $this->type('enum : string { case ASC = "asc"; case DESC = "desc"; - - public static function run($arg) { - return self::from($arg)->name; - } }'); - Assert::equals($expected, $t->getMethod('run')->invoke(null, [$arg])); + Assert::equals($expected, $t->getMethod('from')->invoke(null, [$arg])->name); } #[Test] From 80a531e8cdb30eb4dfe20fadf17163a2a8ad9bbb Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:41:58 +0100 Subject: [PATCH 19/26] Verify XPClass::isEnum() --- src/test/php/lang/ast/unittest/emit/EnumTest.class.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index ef3c78f1..ab46ecd5 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -7,6 +7,11 @@ #[Action(eval: 'new VerifyThat(fn() => function_exists("enum_exists"))')] class EnumTest extends EmittingTest { + #[Test] + public function enum_type() { + Assert::true($this->type('enum { }')->isEnum()); + } + #[Test] public function name_property() { $t= $this->type('enum { From c929bf95ba40c6dcbc5cdcf4aeddb124b6642a51 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:48:21 +0100 Subject: [PATCH 20/26] Verify instance methods work on enums --- .../lang/ast/unittest/emit/EnumTest.class.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index ab46ecd5..461b2981 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -158,6 +158,29 @@ public static function run($arg) { Assert::equals($expected, $t->getMethod('run')->invoke(null, [$arg])); } + #[Test] + public function declare_method_on_enum() { + $t= $this->type('enum { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + + public function color() { + return match ($this) { + self::Hearts, self::Diamonds => "red", + self::Clubs, self::Spaces => "black", + }; + } + + public static function run() { + return self::Hearts->color(); + } + }'); + + Assert::equals('red', $t->getMethod('run')->invoke(null)); + } + #[Test] public function enum_values() { $t= $this->type('enum { From 68b783d83e2dfbf981c9e86faff3bbe1e7c4b47a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 11:48:31 +0100 Subject: [PATCH 21/26] Verify enums can implement interfaces --- .../php/lang/ast/unittest/emit/EnumTest.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 461b2981..795df0f5 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -181,6 +181,18 @@ public static function run() { Assert::equals('red', $t->getMethod('run')->invoke(null)); } + #[Test] + public function enum_implementing_interface() { + $t= $this->type('use lang\Closeable; enum implements Closeable { + case File; + case Stream; + + public function close() { } + }'); + + Assert::true($t->isSubclassOf('lang.Closeable')); + } + #[Test] public function enum_values() { $t= $this->type('enum { From 80958e64d74b30cb67da7f879d83220eb40a156a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 12:07:45 +0100 Subject: [PATCH 22/26] Add TODO comments --- src/main/php/lang/ast/emit/PHP81.class.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index a8cb57b3..6d8d5693 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -41,6 +41,9 @@ protected function emitArguments($result, $arguments) { } protected function emitEnumCase($result, $case) { + + // TODO: Once enum PR is merged, remove this conditional and refactor the + // code into a `RewriteEnums` trait to be included for all other versions if (Type::$ENUMS) { $result->out->write('case '.$case->name); if ($case->expression) { @@ -54,6 +57,9 @@ protected function emitEnumCase($result, $case) { } protected function emitEnum($result, $enum) { + + // TODO: Once enum PR is merged, remove this conditional and refactor the + // code into a `RewriteEnums` trait to be included for all other versions if (Type::$ENUMS) { array_unshift($result->type, $enum); array_unshift($result->meta, []); From e83972ac53e7e9142dba9bd1807a6a66dafc91aa Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 12:14:57 +0100 Subject: [PATCH 23/26] Verify enums and enum cases can be annotated However, since XP core reflection does not support constant annotations, we cannot access case annotations. This will be covered by the reflection library! --- .../php/lang/ast/unittest/emit/EnumTest.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 795df0f5..307f1777 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -193,6 +193,18 @@ public function close() { } Assert::true($t->isSubclassOf('lang.Closeable')); } + #[Test] + public function enum_annotations() { + $t= $this->type('#[Test] enum { }'); + Assert::equals(['test' => null], $t->getAnnotations()); + } + + #[Test, Ignore('XP core reflection does not support constant annotations')] + public function enum_member_annotations() { + $t= $this->type('enum { #[Test] case ONE; }'); + Assert::equals(['test' => null], $t->getConstant('ONE')->getAnnotations()); + } + #[Test] public function enum_values() { $t= $this->type('enum { From 2fc6a5b4168e50ae79cf0a57251d2b4787362a60 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 12:18:19 +0100 Subject: [PATCH 24/26] Verify cases() for backed enums --- .../lang/ast/unittest/emit/EnumTest.class.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php index 307f1777..99630822 100755 --- a/src/test/php/lang/ast/unittest/emit/EnumTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/EnumTest.class.php @@ -29,7 +29,7 @@ public static function run() { } #[Test] - public function cases_method() { + public function cases_method_for_unit_enums() { $t= $this->type('enum { case Hearts; case Diamonds; @@ -43,6 +43,21 @@ public function cases_method() { ); } + #[Test] + public function cases_method_for_backed_enums() { + $t= $this->type('enum : string { + case Hearts = "♥"; + case Diamonds = "♦"; + case Clubs = "♣"; + case Spades = "♠"; + }'); + + Assert::equals( + ['Hearts', 'Diamonds', 'Clubs', 'Spades'], + array_map(function($suit) { return $suit->name; }, $t->getMethod('cases')->invoke(null)) + ); + } + #[Test] public function cases_method_does_not_yield_constants() { $t= $this->type('enum { From 786299f8ccdf6ea88f90fce130e6b8ae1051d9c2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 12:36:21 +0100 Subject: [PATCH 25/26] Restore compatibility with XP < 10.8.0 --- composer.json | 2 +- src/main/php/lang/ast/emit/Reflection.class.php | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 7d86f528..d4497400 100755 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "description" : "XP Compiler", "keywords": ["module", "xp"], "require" : { - "xp-framework/core": "^10.8", + "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", "xp-framework/ast": "dev-feature/php-enums as 7.1.0", "php" : ">=7.0.0" }, diff --git a/src/main/php/lang/ast/emit/Reflection.class.php b/src/main/php/lang/ast/emit/Reflection.class.php index 35cbedcb..f28568d9 100755 --- a/src/main/php/lang/ast/emit/Reflection.class.php +++ b/src/main/php/lang/ast/emit/Reflection.class.php @@ -4,8 +4,11 @@ class Reflection extends Type { private $reflect; + private static $UNITENUM; - static function __static() { } + static function __static() { + self::$UNITENUM= interface_exists(\UnitEnum::class, false); // Compatibility with XP < 10.8.0 + } /** @param string $type */ public function __construct($type) { @@ -28,7 +31,7 @@ public function name() { return $this->reflect->name; } public function rewriteEnumCase($member) { if ($this->reflect->isSubclassOf(Enum::class)) { return $this->reflect->getStaticPropertyValue($member, null) instanceof Enum; - } else if (!self::$ENUMS && $this->reflect->isSubclassOf(\UnitEnum::class)) { + } else if (!self::$ENUMS && self::$UNITENUM && $this->reflect->isSubclassOf(\UnitEnum::class)) { $value= $this->reflect->getConstant($member) ?: $this->reflect->getStaticPropertyValue($member, null); return $value instanceof \UnitEnum; } From d4476daf6b786492d196787972a6fec22c4e098c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 Mar 2021 16:45:54 +0100 Subject: [PATCH 26/26] Use release version for xp-framework/ast --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d4497400..1ca12cae 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0", - "xp-framework/ast": "dev-feature/php-enums as 7.1.0", + "xp-framework/ast": "^7.1", "php" : ">=7.0.0" }, "require-dev" : {