Skip to content

Syntactic support for PHP enums #23

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 9 commits into from
Mar 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ XP AST ChangeLog

## ?.?.? / ????-??-??

## 7.1.0 / ????-??-??

* Merged PR #23: Add syntactic support for PHP 8.1 enums. Implementation
in the compiler is in pull request xp-framework/compiler#106
(@thekid)

## 7.0.4 / 2021-03-07

* Fixed *Call to undefined method ::emitoperator()* caused by standalone
Expand Down
26 changes: 26 additions & 0 deletions src/main/php/lang/ast/nodes/EnumCase.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php namespace lang\ast\nodes;

class EnumCase extends Annotated implements Member {
public $kind= 'enumcase';
public $name, $expression;

public function __construct($name, $expression, $annotations, $line= -1) {
$this->name= $name;
$this->expression= $expression;
$this->annotations= $annotations;
$this->line= $line;
}

/** @return string */
public function lookup() { return $this->name; }

/**
* Checks whether this node is of a given kind
*
* @param string $kind
* @return bool
*/
public function is($kind) {
return $this->kind === $kind || '@member' === $kind || parent::is($kind);
}
}
18 changes: 18 additions & 0 deletions src/main/php/lang/ast/nodes/EnumDeclaration.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php namespace lang\ast\nodes;

use lang\ast\nodes\TypeDeclaration;

class EnumDeclaration extends TypeDeclaration {
public $kind= 'enum';
public $base, $implements;

public function __construct($modifiers, $name, $base, $implements= [], $body= [], $annotations= [], $comment= null, $line= -1) {
parent::__construct($modifiers, $name, $body, $annotations, $comment, $line);
$this->implements= $implements;
$this->base= $base;
}

public function interfaces() { return $this->implements; }

public function case($name) { return $this->body[$name] ?? null; }
}
71 changes: 70 additions & 1 deletion src/main/php/lang/ast/syntax/PHP.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
ContinueStatement,
DoLoop,
EchoStatement,
EnumCase,
EnumDeclaration,
ForLoop,
ForeachLoop,
FunctionDeclaration,
Expand Down Expand Up @@ -860,7 +862,74 @@ public function __construct() {
return $decl;
});

$this->body('use', function($parse, &$body, $annotations, $modifiers, $holder) {
$this->stmt('enum', function($parse, $token) {
$name= $parse->scope->resolve($parse->token->value);
$parse->forward();
$comment= $parse->comment;
$parse->comment= null;

$implements= [];
if ('implements' === $parse->token->value) {
$parse->forward();
do {
$implements[]= $parse->scope->resolve($parse->token->value);
$parse->forward();
if (',' === $parse->token->value) {
$parse->forward();
continue;
} else if ('{' === $parse->token->value) {
break;
} else {
$parse->expecting(', or {', 'interfaces list');
}
} while (null !== $parse->token->value);
}

// Backed enums vs. unit enums
if (':' === $parse->token->value) {
$parse->forward();
$base= $parse->token->value;
$parse->forward();
} else {
$base= null;
}

$decl= new EnumDeclaration([], $name, $base, $implements, [], [], $comment, $token->line);
$parse->expecting('{', 'enum');
$decl->body= $this->typeBody($parse, $decl->name);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not completely correct, as typeBody() allows properties, which PHP enums do not. The RFC states this:

Specifically, the following features of objects are not allowed on enumerations: [...] Enum/Case properties - Properties are a form of state, and enum cases are stateless singletons. Metadata about an enum or case can always be exposed via methods.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll leave this as-is, the compiler may chose to raise errors about this; or we simply leave it to the PHP runtime.

$parse->expecting('}', 'enum');

return $decl;
});

$this->body('case', function($parse, &$body, $meta, $modifiers, $holder) {
$parse->forward();
do {
$line= $parse->token->line;
$name= $parse->token->value;

$parse->forward();
if ('=' === $parse->token->value) {
$parse->forward();
$expr= $this->expression($parse, 0);
} else {
$expr= null;
}

$body[$name]= new EnumCase($name, $expr, $meta[DETAIL_ANNOTATIONS] ?? [], $line);
$body[$name]->holder= $holder;

if (',' === $parse->token->value) {
$parse->forward();
continue;
} else {
$parse->expecting(';', 'case');
break;
}
} while ($parse->token->value);
});

$this->body('use', function($parse, &$body, $meta, $modifiers, $holder) {
$line= $parse->token->line;

$parse->forward();
Expand Down
11 changes: 11 additions & 0 deletions src/test/php/lang/ast/unittest/parse/AttributesTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ public function on_interface($attributes, $expected) {
$this->assertAnnotated($expected, $this->type($attributes.' interface T { }'));
}

#[Test, Values('attributes')]
public function on_enum($attributes, $expected) {
$this->assertAnnotated($expected, $this->type($attributes.' enum T { }'));
}

#[Test, Values('attributes')]
public function on_constant($attributes, $expected) {
$type= $this->type('class T { '.$attributes.' const FIXTURE = 1; }');
Expand All @@ -222,4 +227,10 @@ public function on_parameter($attributes, $expected) {
$type= $this->type('class T { public function fixture('.$attributes.' $p) { } }');
$this->assertAnnotated($expected, $type->method('fixture')->signature->parameters[0]);
}

#[Test, Values('attributes')]
public function on_enum_case($attributes, $expected) {
$type= $this->type('enum T { '.$attributes.' case ONE; }');
$this->assertAnnotated($expected, $type->case('ONE'));
}
}
51 changes: 50 additions & 1 deletion src/test/php/lang/ast/unittest/parse/TypesTest.class.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
<?php namespace lang\ast\unittest\parse;

use lang\ast\Errors;
use lang\ast\nodes\{ClassDeclaration, InterfaceDeclaration, NamespaceDeclaration, TraitDeclaration, UseExpression};
use lang\ast\nodes\{
ClassDeclaration,
InterfaceDeclaration,
EnumDeclaration,
EnumCase,
NamespaceDeclaration,
TraitDeclaration,
UseExpression,
Literal
};
use unittest\{Assert, Expect, Test};

class TypesTest extends ParseTest {
Expand Down Expand Up @@ -86,6 +95,46 @@ public function empty_trait() {
);
}

#[Test]
public function empty_unit_enum() {
$this->assertParsed(
[new EnumDeclaration([], '\\A', null, [], [], [], null, self::LINE)],
'enum A { }'
);
}

#[Test]
public function empty_backed_enum() {
$this->assertParsed(
[new EnumDeclaration([], '\\A', 'string', [], [], [], null, self::LINE)],
'enum A: string { }'
);
}

#[Test]
public function unit_enum_with_cases() {
$enum= new EnumDeclaration([], '\\A', null, [], [], [], null, self::LINE);
$enum->declare(new EnumCase('ONE', null, [], self::LINE));
$enum->declare(new EnumCase('TWO', null, [], self::LINE));
$this->assertParsed([$enum], 'enum A { case ONE; case TWO; }');
}

#[Test]
public function backed_enum_with_cases() {
$enum= new EnumDeclaration([], '\\A', 'int', [], [], [], null, self::LINE);
$enum->declare(new EnumCase('ONE', new Literal('1', self::LINE), [], self::LINE));
$enum->declare(new EnumCase('TWO', new Literal('2', self::LINE), [], self::LINE));
$this->assertParsed([$enum], 'enum A: int { case ONE = 1; case TWO = 2; }');
}

#[Test]
public function unit_enum_with_grouped_cases() {
$enum= new EnumDeclaration([], '\\A', null, [], [], [], null, self::LINE);
$enum->declare(new EnumCase('ONE', null, [], self::LINE));
$enum->declare(new EnumCase('TWO', null, [], self::LINE));
$this->assertParsed([$enum], 'enum A { case ONE, TWO; }');
}

#[Test]
public function class_with_trait() {
$class= new ClassDeclaration([], '\\A', null, [], [], [], null, self::LINE);
Expand Down