Skip to content

Commit

Permalink
Merge pull request #183 from xp-framework/feature/asymmetric-visibility
Browse files Browse the repository at this point in the history
Add emitting support for asymmetric visibility
  • Loading branch information
thekid authored Aug 27, 2024
2 parents f55a373 + c69c867 commit f121e86
Show file tree
Hide file tree
Showing 13 changed files with 299 additions and 44 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"require" : {
"xp-framework/core": "^12.0 | ^11.6 | ^10.16",
"xp-framework/reflection": "^3.2 | ^2.15",
"xp-framework/ast": "^11.1",
"xp-framework/ast": "^11.3",
"php" : ">=7.4.0"
},
"require-dev" : {
Expand Down
81 changes: 81 additions & 0 deletions src/main/php/lang/ast/emit/AsymmetricVisibility.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php namespace lang\ast\emit;

use lang\ast\nodes\{
Assignment,
Block,
InstanceExpression,
Literal,
OffsetExpression,
ReturnStatement,
Variable
};

/**
* Asymmetric Visibility
*
* @see https://wiki.php.net/rfc/asymmetric-visibility-v2
* @test lang.ast.unittest.emit.AsymmetricVisibilityTest
*/
trait AsymmetricVisibility {
use VisibilityChecks;

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' => MODIFIER_READONLY,
'public(set)' => 0x1000000,
'protected(set)' => 0x0000800,
'private(set)' => 0x0001000,
];

$scope= $result->codegen->scope[0];
$modifiers= 0;
foreach ($property->modifiers as $name) {
$modifiers|= $lookup[$name];
}

// Declare checks for private(set) and protected(set), folding declarations
// like `[visibility] [visibility](set)` to just the visibility itself.
if ($modifiers & 0x1000000) {
$checks= [];
$modifiers&= ~0x1000000;
} else if ($modifiers & 0x0000800) {
$checks= [$this->protected($property->name, 'modify protected(set)')];
$modifiers & MODIFIER_PROTECTED && $modifiers&= ~0x0000800;
} else if ($modifiers & 0x0001000) {
$checks= [$this->private($property->name, 'modify private(set)')];
$modifiers & MODIFIER_PRIVATE && $modifiers&= ~0x0001000;
}

// Emit XP meta information for the reflection API
$scope->meta[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]
];

// The readonly flag is really two flags in one: write-once and restricted(set)
if (in_array('readonly', $property->modifiers)) {
$checks[]= $this->initonce($property->name);
}

$virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(
new Literal('__virtual'),
new Literal("'{$property->name}'"))
);
$scope->virtual[$property->name]= [
new ReturnStatement($virtual),
new Block([...$checks, new Assignment($virtual, '=', new Variable('value'))]),
];
if (isset($property->expression)) {
$scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression;
}
}
}
2 changes: 2 additions & 0 deletions src/main/php/lang/ast/emit/PHP74.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class PHP74 extends PHP {
RewriteThrowableExpressions
;

public $targetVersion= 70400;

/** Sets up type => literal mappings */
public function __construct() {
$this->literals= [
Expand Down
2 changes: 2 additions & 0 deletions src/main/php/lang/ast/emit/PHP80.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class PHP80 extends PHP {
RewriteStaticVariableInitializations
;

public $targetVersion= 80000;

/** Sets up type => literal mappings */
public function __construct() {
$this->literals= [
Expand Down
6 changes: 4 additions & 2 deletions src/main/php/lang/ast/emit/PHP81.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ class PHP81 extends PHP {
RewriteBlockLambdaExpressions,
RewriteDynamicClassConstants,
RewriteStaticVariableInitializations,
RewriteProperties,
ReadonlyClasses,
OmitConstantTypes,
PropertyHooks
OmitConstantTypes
;

public $targetVersion= 80100;

/** Sets up type => literal mappings */
public function __construct() {
$this->literals= [
Expand Down
6 changes: 4 additions & 2 deletions src/main/php/lang/ast/emit/PHP82.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ class PHP82 extends PHP {
RewriteBlockLambdaExpressions,
RewriteDynamicClassConstants,
RewriteStaticVariableInitializations,
RewriteProperties,
ReadonlyClasses,
OmitConstantTypes,
PropertyHooks
OmitConstantTypes
;

public $targetVersion= 80200;

/** Sets up type => literal mappings */
public function __construct() {
$this->literals= [
Expand Down
4 changes: 3 additions & 1 deletion src/main/php/lang/ast/emit/PHP83.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
* @see https://wiki.php.net/rfc#php_83
*/
class PHP83 extends PHP {
use RewriteBlockLambdaExpressions, ReadonlyClasses, PropertyHooks;
use RewriteBlockLambdaExpressions, RewriteProperties, ReadonlyClasses;

public $targetVersion= 80300;

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

use ReflectionProperty;
use lang\ast\Code;
use lang\ast\nodes\{
Assignment,
Block,
Expand All @@ -24,6 +23,7 @@
* @test lang.ast.unittest.emit.PropertyHooksTest
*/
trait PropertyHooks {
use VisibilityChecks;

protected function rewriteHook($node, $name, $virtual, $literal) {

Expand Down Expand Up @@ -61,24 +61,14 @@ protected function rewriteHook($node, $name, $virtual, $literal) {

protected function withScopeCheck($modifiers, $nodes) {
if ($modifiers & MODIFIER_PRIVATE) {
$check= (
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'.
'throw new \\Error("Cannot access private property ".__CLASS__."::".$name);'
);
return new Block([$this->private('$name', 'access private'), ...$nodes]);
} else if ($modifiers & MODIFIER_PROTECTED) {
$check= (
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'.
'throw new \\Error("Cannot access protected property ".__CLASS__."::".$name);'
);
return new Block([$this->protected('$name', 'access protected'), ...$nodes]);
} else if (1 === sizeof($nodes)) {
return $nodes[0];
} else {
return new Block($nodes);
}

return new Block([new Code($check), ...$nodes]);
}

protected function emitEmulatedHooks($result, $property) {
Expand Down
55 changes: 33 additions & 22 deletions src/main/php/lang/ast/emit/ReadonlyProperties.class.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
<?php namespace lang\ast\emit;

use lang\ast\nodes\{
Assignment,
Block,
InstanceExpression,
InvokeExpression,
Literal,
OffsetExpression,
ReturnStatement,
Variable
};
use lang\ast\{Code, Error, Errors};

/**
Expand All @@ -9,6 +19,7 @@
* @see https://wiki.php.net/rfc/readonly_properties_v2
*/
trait ReadonlyProperties {
use VisibilityChecks;

protected function emitProperty($result, $property) {
static $lookup= [
Expand Down Expand Up @@ -37,33 +48,33 @@ protected function emitProperty($result, $property) {
];

// Add visibility check for accessing private and protected properties
if (in_array('private', $property->modifiers)) {
$check= (
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'.
'throw new \\Error("Cannot access private property ".__CLASS__."::\\$%1$s");'
);
} else if (in_array('protected', $property->modifiers)) {
$check= (
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'.
'throw new \\Error("Cannot access protected property ".__CLASS__."::\\$%1$s");'
);
if ($modifiers & MODIFIER_PRIVATE) {
$check= $this->private($property->name, 'access private');
} else if ($modifiers & MODIFIER_PROTECTED) {
$check= $this->protected($property->name, 'access protected');
} else {
$check= '';
$check= null;
}

$virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(
new Literal('__virtual'),
new Literal("'{$property->name}'"))
);

// Create virtual property implementing the readonly semantics
$scope->virtual[$property->name]= [
new Code(sprintf($check.'return $this->__virtual["%1$s"][0];', $property->name)),
new Code(sprintf(
($check ?: '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;').
'if (isset($this->__virtual["%1$s"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::{$name}");'.
'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'.
'throw new \\Error("Cannot initialize readonly property ".__CLASS__."::{$name} from ".($scope ? "scope {$scope}": "global scope"));'.
'$this->__virtual["%1$s"]= [$value];',
$property->name
)),
$check ? new Block([$check, new ReturnStatement($virtual)]) : new ReturnStatement($virtual),
new Block([
$check ?? new Code('$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'),
$this->initonce($property->name),
new Code(sprintf(
'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'.
'throw new \\Error("Cannot initialize readonly property ".__CLASS__."::{$name} from ".($scope ? "scope {$scope}": "global scope"));'.
'$this->__virtual["%1$s"]= [$value];',
$property->name
)),
new Assignment($virtual, '=', new Variable('value'))
]),
];
}
}
15 changes: 13 additions & 2 deletions src/main/php/lang/ast/emit/RewriteProperties.class.php
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
<?php namespace lang\ast\emit;

use ReflectionProperty;

trait RewriteProperties {
use PropertyHooks, ReadonlyProperties {
use PropertyHooks, ReadonlyProperties, AsymmetricVisibility {
PropertyHooks::emitProperty as emitPropertyHooks;
ReadonlyProperties::emitProperty as emitReadonlyProperties;
AsymmetricVisibility::emitProperty as emitAsymmetricVisibility;
}

protected function emitProperty($result, $property) {
if ($property->hooks) {
return $this->emitPropertyHooks($result, $property);
} else if (in_array('readonly', $property->modifiers)) {
} else if (
$this->targetVersion < 80400 &&
array_intersect($property->modifiers, ['private(set)', 'protected(set)', 'public(set)'])
) {
return $this->emitAsymmetricVisibility($result, $property);
} else if (
$this->targetVersion < 80100 &&
in_array('readonly', $property->modifiers)
) {
return $this->emitReadonlyProperties($result, $property);
}
parent::emitProperty($result, $property);
Expand Down
26 changes: 26 additions & 0 deletions src/main/php/lang/ast/emit/VisibilityChecks.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php namespace lang\ast\emit;

use lang\ast\Code;

trait VisibilityChecks {

private function initonce($name) {
return new Code('if (isset($this->__virtual["'.$name.'"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::\$'.$name.'");');
}

private function private($name, $access) {
return new Code(
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'.
'throw new \\Error("Cannot '.$access.' property ".__CLASS__."::\$'.$name.' from ".($scope ? "scope ".$scope : "global scope"));'
);
}

private function protected($name, $access) {
return new Code(
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'.
'throw new \\Error("Cannot '.$access.' property ".__CLASS__."::\$'.$name.' from ".($scope ? "scope ".$scope : "global scope"));'
);
}
}
Loading

0 comments on commit f121e86

Please sign in to comment.