Skip to content

Add support for readonly properties #124

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 5 commits into from
Sep 11, 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
59 changes: 43 additions & 16 deletions src/main/php/lang/ast/emit/PHP.class.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php namespace lang\ast\emit;

use lang\ast\Code;
use lang\ast\nodes\{InstanceExpression, ScopeExpression, BinaryExpression, Variable, Literal, ArrayLiteral, Block};
use lang\ast\nodes\{InstanceExpression, ScopeExpression, BinaryExpression, Variable, Literal, ArrayLiteral, Block, Property};
use lang\ast\types\{IsUnion, IsFunction, IsArray, IsMap};
use lang\ast\{Emitter, Node, Type};

Expand Down Expand Up @@ -397,7 +397,7 @@ protected function emitEnum($result, $enum) {
protected function emitClass($result, $class) {
array_unshift($result->type, $class);
array_unshift($result->meta, []);
$result->locals= [[], []];
$result->locals= [[], [], []];

$result->out->write(implode(' ', $class->modifiers).' class '.$this->declaration($class->name));
$class->parent && $result->out->write(' extends '.$class->parent);
Expand All @@ -407,6 +407,31 @@ protected function emitClass($result, $class) {
$this->emitOne($result, $member);
}

// Virtual properties support: __virtual member + __get() and __set()
if ($result->locals[2]) {
$result->out->write('private $__virtual= [');
foreach ($result->locals[2] as $name => $access) {
$result->out->write("'{$name}' => null,");
}
$result->out->write('];');

$result->out->write('public function __get($name) { switch ($name) {');
foreach ($result->locals[2] as $name => $access) {
$result->out->write('case "'.$name.'":');
$this->emitOne($result, $access[0]);
$result->out->write('break;');
}
$result->out->write('default: trigger_error("Undefined property ".__CLASS__."::".$name, E_USER_WARNING); }}');

$result->out->write('public function __set($name, $value) { switch ($name) {');
foreach ($result->locals[2] as $name => $access) {
$result->out->write('case "'.$name.'":');
$this->emitOne($result, $access[1]);
$result->out->write('break;');
}
$result->out->write('}}');
}

// Create constructor for property initializations to non-static scalars
// which have not already been emitted inside constructor
if ($result->locals[1]) {
Expand Down Expand Up @@ -496,7 +521,10 @@ protected function emitMeta($result, $name, $annotations, $comment) {
$this->attributes($result, $meta[DETAIL_ANNOTATIONS], $meta[DETAIL_TARGET_ANNO]);
$result->out->write(', DETAIL_RETURNS => \''.$meta[DETAIL_RETURNS].'\'');
$result->out->write(', DETAIL_COMMENT => '.$this->comment($meta[DETAIL_COMMENT]));
$result->out->write(', DETAIL_ARGUMENTS => [\''.implode('\', \'', $meta[DETAIL_ARGUMENTS]).'\']],');
$result->out->write(', DETAIL_ARGUMENTS => ['.($meta[DETAIL_ARGUMENTS]
? "'".implode("', '", $meta[DETAIL_ARGUMENTS])."']],"
: ']],'
));
}
$result->out->write('],');
}
Expand Down Expand Up @@ -577,10 +605,10 @@ protected function emitMethod($result, $method) {
// Include non-static initializations for members in constructor, removing
// them to signal emitClass() no constructor needs to be generated.
if ('__construct' === $method->name && isset($result->locals[1])) {
$locals= ['this' => true, 1 => $result->locals[1]];
$locals= [[], $result->locals[1], [], 'this' => true];
$result->locals[1]= [];
} else {
$locals= ['this' => true, 1 => []];
$locals= [[], [], [], 'this' => true];
}
$result->stack[]= $result->locals;
$result->locals= $locals;
Expand All @@ -596,21 +624,16 @@ protected function emitMethod($result, $method) {
$result->out->write(implode(' ', $method->modifiers).' function '.$method->name);
$this->emitSignature($result, $method->signature);

$promoted= '';
$promoted= [];
foreach ($method->signature->parameters as $param) {
$meta[DETAIL_TARGET_ANNO][$param->name]= $param->annotations;
$meta[DETAIL_ARGUMENTS][]= $param->type ? $param->type->name() : 'var';

// Create properties from promoted parameters. Do not include default value, this is handled
// in emitParameter() already; otherwise we would be emitting it twice.
if (isset($param->promote)) {
$promoted.= $param->promote.' '.$this->propertyType($param->type).' $'.$param->name.';';
$promoted[]= new Property(explode(' ', $param->promote), $param->name, $param->type, null, [], null, $param->line);
$result->locals[1]['$this->'.$param->name]= new Code(($param->reference ? '&$' : '$').$param->name);
$result->meta[0][self::PROPERTY][$param->name]= [
DETAIL_RETURNS => $param->type ? $param->type->name() : 'var',
DETAIL_ANNOTATIONS => [],
DETAIL_COMMENT => null,
DETAIL_TARGET_ANNO => [],
DETAIL_ARGUMENTS => []
];
}

if (isset($param->default) && !$this->isConstant($result, $param->default)) {
Expand All @@ -627,9 +650,13 @@ protected function emitMethod($result, $method) {
$result->out->write('}');
}

$result->out->write($promoted);
foreach ($promoted as $property) {
$this->emitOne($result, $property);
}

// Copy any virtual properties inside locals[2] to class scope
$result->locals= [2 => $result->locals[2]] + array_pop($result->stack);
$result->meta[0][self::METHOD][$method->name]= $meta;
$result->locals= array_pop($result->stack);
}

protected function emitBraced($result, $braced) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP70.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* @see https://wiki.php.net/rfc#php_70
*/
class PHP70 extends PHP {
use OmitPropertyTypes, OmitConstModifiers;
use OmitPropertyTypes, OmitConstModifiers, ReadonlyProperties;
use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteMultiCatch, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums;

/** Sets up type => literal mappings */
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP71.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* @see https://wiki.php.net/rfc#php_71
*/
class PHP71 extends PHP {
use OmitPropertyTypes, CallablesAsClosures;
use OmitPropertyTypes, CallablesAsClosures, ReadonlyProperties;
use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums;

/** Sets up type => literal mappings */
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP72.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* @see https://wiki.php.net/rfc#php_72
*/
class PHP72 extends PHP {
use OmitPropertyTypes, CallablesAsClosures;
use OmitPropertyTypes, CallablesAsClosures, ReadonlyProperties;
use RewriteNullCoalesceAssignment, RewriteLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums;

/** Sets up type => literal mappings */
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP74.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* @see https://wiki.php.net/rfc#php_74
*/
class PHP74 extends PHP {
use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums, CallablesAsClosures;
use RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteExplicitOctals, RewriteEnums, CallablesAsClosures, ReadonlyProperties;

/** Sets up type => literal mappings */
public function __construct() {
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP80.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* @see https://wiki.php.net/rfc#php_80
*/
class PHP80 extends PHP {
use RewriteBlockLambdaExpressions, RewriteExplicitOctals, RewriteEnums, CallablesAsClosures;
use RewriteBlockLambdaExpressions, RewriteExplicitOctals, RewriteEnums, ReadonlyProperties, CallablesAsClosures;

/** Sets up type => literal mappings */
public function __construct() {
Expand Down
68 changes: 68 additions & 0 deletions src/main/php/lang/ast/emit/ReadonlyProperties.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php namespace lang\ast\emit;

use lang\ast\Code;

/**
* Creates __get() and __set() overloads for readonly properties
*
* @see https://github.com/xp-framework/compiler/issues/115
* @see https://wiki.php.net/rfc/readonly_properties_v2
*/
trait ReadonlyProperties {

protected function emitProperty($result, $property) {
static $lookup= [
'public' => MODIFIER_PUBLIC,
'protected' => MODIFIER_PROTECTED,
'private' => MODIFIER_PRIVATE,
'static' => MODIFIER_STATIC,
'final' => MODIFIER_FINAL,
'abstract' => MODIFIER_ABSTRACT,
'readonly' => 0x0080, // XP 10.13: MODIFIER_READONLY
];

if (!in_array('readonly', $property->modifiers)) return parent::emitProperty($result, $property);

$modifiers= 0;
foreach ($property->modifiers as $name) {
$modifiers|= $lookup[$name];
}
$result->meta[0][self::PROPERTY][$property->name]= [
DETAIL_RETURNS => $property->type ? $property->type->name() : 'var',
DETAIL_ANNOTATIONS => $property->annotations,
DETAIL_COMMENT => $property->comment,
DETAIL_TARGET_ANNO => [],
DETAIL_ARGUMENTS => [$modifiers]
];

// Create virtual property implementing the readonly semantics
$result->locals[2][$property->name]= [
new Code('return $this->__virtual["'.$property->name.'"][0] ?? null;'),
new Code('
if (isset($this->__virtual["'.$property->name.'"])) {
throw new \\Error("Cannot modify readonly property ".__CLASS__."::{$name}");
}
$caller= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1];
$scope= $caller["class"] ?? null;
if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope) {
throw new \\Error("Cannot initialize readonly property ".__CLASS__."::{$name} from ".($scope
? "scope {$scope}"
: "global scope"
));
}
$this->__virtual["'.$property->name.'"]= [$value];
'),
];

if (isset($property->expression)) {
if ($this->isConstant($result, $property->expression)) {
$result->out->write('=');
$this->emitOne($result, $property->expression);
} else if (in_array('static', $property->modifiers)) {
$result->locals[0]['self::$'.$property->name]= $property->expression;
} else {
$result->locals[1]['$this->'.$property->name]= $property->expression;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php namespace lang\ast\unittest\emit;

use lang\Error;
use unittest\{Assert, Test};

/**
* Readonly properties
*
* @see https://wiki.php.net/rfc/readonly_properties_v2
*/
class ReadonlyPropertiesTest extends EmittingTest {

#[Test]
public function declaration() {
$t= $this->type('class <T> {
public readonly int $fixture;
}');

Assert::equals(
sprintf('public readonly int %s::$fixture', $t->getName()),
$t->getField('fixture')->toString()
);
}

#[Test]
public function with_constructor_argument_promotion() {
$t= $this->type('class <T> {
public function __construct(public readonly string $fixture) { }
}');

Assert::equals('Test', $t->newInstance('Test')->fixture);
}

#[Test]
public function reading() {
$t= $this->type('class <T> {
public function __construct(public readonly string $fixture) { }
}');
Assert::equals('Test', $t->newInstance('Test')->fixture);
}

#[Test]
public function assigning_inside_constructor() {
$t= $this->type('class <T> {
public readonly string $fixture;
public function __construct($fixture) { $this->fixture= $fixture; }
}');
Assert::equals('Test', $t->newInstance('Test')->fixture);
}

#[Test]
public function can_be_assigned_via_reflection() {
$t= $this->type('class <T> {
public readonly string $fixture;
}');
$i= $t->newInstance();
$t->getField('fixture')->setAccessible(true)->set($i, 'Test');

Assert::equals('Test', $i->fixture);
}

#[Test, Expect(class: Error::class, withMessage: '/Cannot modify readonly property .+fixture/')]
public function cannot_be_set_after_initialization() {
$t= $this->type('class <T> {
public function __construct(public readonly string $fixture) { }
}');
$t->newInstance('Test')->fixture= 'Modified';
}
}