From f6d6a62d3cb11ff18a9c94f3cf77a002c723dd42 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 May 2023 21:55:03 +0200 Subject: [PATCH 01/30] Implement property hooks via virtual properties --- composer.json | 2 +- src/main/php/lang/ast/emit/PHP70.class.php | 2 +- src/main/php/lang/ast/emit/PHP71.class.php | 2 +- src/main/php/lang/ast/emit/PHP72.class.php | 2 +- src/main/php/lang/ast/emit/PHP74.class.php | 2 +- src/main/php/lang/ast/emit/PHP80.class.php | 4 +- src/main/php/lang/ast/emit/PHP81.class.php | 2 +- src/main/php/lang/ast/emit/PHP82.class.php | 2 +- src/main/php/lang/ast/emit/PHP83.class.php | 2 +- .../php/lang/ast/emit/PropertyHooks.class.php | 98 ++++++++++ .../lang/ast/emit/RewriteProperties.class.php | 17 ++ .../unittest/emit/PropertyHooksTest.class.php | 182 ++++++++++++++++++ 12 files changed, 307 insertions(+), 10 deletions(-) create mode 100755 src/main/php/lang/ast/emit/PropertyHooks.class.php create mode 100755 src/main/php/lang/ast/emit/RewriteProperties.class.php create mode 100755 src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php diff --git a/composer.json b/composer.json index 4330dcfb..2b16785b 100755 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "keywords": ["module", "xp"], "require" : { "xp-framework/core": "^11.0 | ^10.0", - "xp-framework/ast": "^10.0", + "xp-framework/ast": "dev-feature/property-hooks as 10.1.0", "php" : ">=7.0.0" }, "require-dev" : { diff --git a/src/main/php/lang/ast/emit/PHP70.class.php b/src/main/php/lang/ast/emit/PHP70.class.php index 84c55b61..6422a4ce 100755 --- a/src/main/php/lang/ast/emit/PHP70.class.php +++ b/src/main/php/lang/ast/emit/PHP70.class.php @@ -22,13 +22,13 @@ class PHP70 extends PHP { OmitConstantTypes, OmitPropertyTypes, ReadonlyClasses, - ReadonlyProperties, RewriteAssignments, RewriteClassOnObjects, RewriteEnums, RewriteExplicitOctals, RewriteLambdaExpressions, RewriteMultiCatch, + RewriteProperties, RewriteThrowableExpressions ; diff --git a/src/main/php/lang/ast/emit/PHP71.class.php b/src/main/php/lang/ast/emit/PHP71.class.php index 53324abb..a9986a00 100755 --- a/src/main/php/lang/ast/emit/PHP71.class.php +++ b/src/main/php/lang/ast/emit/PHP71.class.php @@ -20,13 +20,13 @@ class PHP71 extends PHP { OmitArgumentNames, OmitConstantTypes, OmitPropertyTypes, - ReadonlyProperties, ReadonlyClasses, RewriteAssignments, RewriteClassOnObjects, RewriteEnums, RewriteExplicitOctals, RewriteLambdaExpressions, + RewriteProperties, RewriteThrowableExpressions ; diff --git a/src/main/php/lang/ast/emit/PHP72.class.php b/src/main/php/lang/ast/emit/PHP72.class.php index 9e92eb91..edb53abc 100755 --- a/src/main/php/lang/ast/emit/PHP72.class.php +++ b/src/main/php/lang/ast/emit/PHP72.class.php @@ -20,13 +20,13 @@ class PHP72 extends PHP { OmitArgumentNames, OmitConstantTypes, OmitPropertyTypes, - ReadonlyProperties, ReadonlyClasses, RewriteAssignments, RewriteClassOnObjects, RewriteEnums, RewriteExplicitOctals, RewriteLambdaExpressions, + RewriteProperties, RewriteThrowableExpressions ; diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index da2f0e64..3192c8a1 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -19,11 +19,11 @@ class PHP74 extends PHP { OmitArgumentNames, OmitConstantTypes, ReadonlyClasses, - ReadonlyProperties, RewriteBlockLambdaExpressions, RewriteClassOnObjects, RewriteEnums, RewriteExplicitOctals, + RewriteProperties, RewriteThrowableExpressions ; diff --git a/src/main/php/lang/ast/emit/PHP80.class.php b/src/main/php/lang/ast/emit/PHP80.class.php index 5783ea5c..4ca04f5f 100755 --- a/src/main/php/lang/ast/emit/PHP80.class.php +++ b/src/main/php/lang/ast/emit/PHP80.class.php @@ -23,11 +23,11 @@ class PHP80 extends PHP { CallablesAsClosures, OmitConstantTypes, ReadonlyClasses, - ReadonlyProperties, RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, RewriteEnums, - RewriteExplicitOctals + RewriteExplicitOctals, + RewriteProperties ; /** Sets up type => literal mappings */ diff --git a/src/main/php/lang/ast/emit/PHP81.class.php b/src/main/php/lang/ast/emit/PHP81.class.php index 83b8cbde..fbd5e441 100755 --- a/src/main/php/lang/ast/emit/PHP81.class.php +++ b/src/main/php/lang/ast/emit/PHP81.class.php @@ -19,7 +19,7 @@ * @see https://wiki.php.net/rfc#php_81 */ class PHP81 extends PHP { - use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, ReadonlyClasses, OmitConstantTypes; + use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, ReadonlyClasses, OmitConstantTypes, PropertyHooks; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP82.class.php b/src/main/php/lang/ast/emit/PHP82.class.php index be3ae3b6..52160971 100755 --- a/src/main/php/lang/ast/emit/PHP82.class.php +++ b/src/main/php/lang/ast/emit/PHP82.class.php @@ -19,7 +19,7 @@ * @see https://wiki.php.net/rfc#php_82 */ class PHP82 extends PHP { - use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, ReadonlyClasses, OmitConstantTypes; + use RewriteBlockLambdaExpressions, RewriteDynamicClassConstants, ReadonlyClasses, OmitConstantTypes, PropertyHooks; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PHP83.class.php b/src/main/php/lang/ast/emit/PHP83.class.php index 3be2fb23..9285e65d 100755 --- a/src/main/php/lang/ast/emit/PHP83.class.php +++ b/src/main/php/lang/ast/emit/PHP83.class.php @@ -18,7 +18,7 @@ * @see https://wiki.php.net/rfc#php_83 */ class PHP83 extends PHP { - use RewriteBlockLambdaExpressions, ReadonlyClasses; + use RewriteBlockLambdaExpressions, ReadonlyClasses, PropertyHooks; /** Sets up type => literal mappings */ public function __construct() { diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php new file mode 100755 index 00000000..ed27a2d9 --- /dev/null +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -0,0 +1,98 @@ + 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 (empty($property->hooks)) return parent::emitProperty($result, $property); + + $virtual= new InstanceExpression( + new Variable('this'), + new OffsetExpression(new Literal('__virtual'), new Literal("'{$property->name}'")) + ); + + // Execute `set` hook, then assign virtual property to special variable $field + if ($hook= $property->hooks['set'] ?? null) { + $set= new Block([$hook->expression, new Assignment($virtual, '=', new Variable('field'))]); + + if ($hook->parameter) { + if ('value' !== $hook->parameter->name) { + array_unshift($set->statements, new Assignment( + new Variable($hook->parameter->name), + '=', + new Variable('value') + )); + } + + // Perform type checking if parameter is typed using an IIFE + if ($hook->parameter->type) { + array_unshift($set->statements, new InvokeExpression( + new Braced(new ClosureExpression(new Signature([$hook->parameter], null), [], [])), + [new Variable('value')] + )); + } + } + } else { + $set= new Assignment($virtual, '=', new Variable('value')); + } + + // Assign special variable $field to virtual property, then execute `get` hook + if ($hook= $property->hooks['get'] ?? null) { + $get= new Block([ + new Assignment(new Variable('field'), '=', $virtual), + $hook->expression instanceof Block ? $hook->expression : new ReturnStatement($hook->expression) + ]); + } else { + $get= new ReturnStatement($virtual); + } + + $scope= $result->codegen->scope[0]; + $scope->virtual[$property->name]= [$get, $set]; + + // Initialize via constructor + if (isset($property->expression)) { + $scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression; + } + + // Emit XP meta information for the reflection API + $modifiers= 0; + foreach ($property->modifiers as $name) { + $modifiers|= $lookup[$name]; + } + $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] + ]; + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/RewriteProperties.class.php b/src/main/php/lang/ast/emit/RewriteProperties.class.php new file mode 100755 index 00000000..29a0b4d6 --- /dev/null +++ b/src/main/php/lang/ast/emit/RewriteProperties.class.php @@ -0,0 +1,17 @@ +hooks) { + return $this->emitPropertyHooks($result, $property); + } else if (in_array('readonly', $property->modifiers)) { + return $this->emitReadonlyProperties($result, $property); + } + parent::emitProperty($result, $property); + } +} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php new file mode 100755 index 00000000..f246550c --- /dev/null +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -0,0 +1,182 @@ +run('class { + public $test { get => "Test"; } + + public function run() { + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function get_block() { + $r= $this->run('class { + public $test { get { return "Test"; } } + + public function run() { + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } +# + #[Test] + public function abbreviated_get() { + $r= $this->run('class { + private $word= "Test"; + private $interpunction= "!"; + + public $test => $this->word.$this->interpunction; + + public function run() { + return $this->test; + } + }'); + + Assert::equals('Test!', $r); + } + + #[Test] + public function set_expression() { + $r= $this->run('class { + public $test { set => $field= ucfirst($value); } + + public function run() { + $this->test= "test"; + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function set_block() { + $r= $this->run('class { + public $test { set($value) { $field= ucfirst($value); } } + + public function run() { + $this->test= "test"; + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test, Expect(IllegalArgumentException::class)] + public function set_raising_exception() { + $this->run('use lang\\IllegalArgumentException; class { + public $test { set($value) { throw new IllegalArgumentException("Cannot set"); } } + + public function run() { + $this->test= "test"; + } + }'); + } + + #[Test] + public function get_and_set_using_field() { + $r= $this->run('class { + public $test { + get => $field; + set => $field= ucfirst($value); + } + + public function run() { + $this->test= "test"; + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function implicit_set() { + $r= $this->run('class { + public $test { + get => ucfirst($field); + } + + public function run() { + $this->test= "test"; + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function typed_set() { + $r= $this->run('use util\\Bytes; class { + public string $test { + set(string|Bytes $arg) => $field= ucfirst($arg); + } + + public function run() { + $this->test= new Bytes(["t", "e", "s", "t"]); + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test, Expect(class: Error::class, message: '/Argument .+ type int(eger)?, string given/')] + public function typed_mismatch() { + $this->run('class { + public string $test { + set(int $times) => $field= $times." times"; + } + + public function run() { + $this->test= "no"; + } + }'); + } + + #[Test] + public function initial_value() { + $r= $this->run('class { + public $test= "test" { + get => ucfirst($field); + } + + public function run() { + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function reflection() { + $t= $this->type('class { + public string $test { + get => $field; + set => $field= ucfirst($value); + } + }'); + + Assert::equals('public string '.$t->getName().'::$test', $t->getField('test')->toString()); + } +} \ No newline at end of file From 8abde260f64712cfbecfdb765ccb5c850a761422 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 13 May 2023 22:02:30 +0200 Subject: [PATCH 02/30] Fix PHP 7.3 --- src/main/php/lang/ast/emit/PHP73.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PHP73.class.php b/src/main/php/lang/ast/emit/PHP73.class.php index 820b6e99..ac3dba4d 100755 --- a/src/main/php/lang/ast/emit/PHP73.class.php +++ b/src/main/php/lang/ast/emit/PHP73.class.php @@ -21,12 +21,12 @@ class PHP73 extends PHP { OmitArgumentNames, OmitConstantTypes, OmitPropertyTypes, - ReadonlyProperties, ReadonlyClasses, RewriteClassOnObjects, RewriteEnums, RewriteExplicitOctals, RewriteLambdaExpressions, + RewriteProperties, RewriteThrowableExpressions ; From 87f629c37a16284ee891988ab55e3d1b5bf2e203 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 14 May 2023 10:45:55 +0200 Subject: [PATCH 03/30] Add support for `__PROPERTY__` --- .../php/lang/ast/emit/PropertyHooks.class.php | 34 +++++++++++-------- .../unittest/emit/PropertyHooksTest.class.php | 13 +++++++ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index ed27a2d9..57b9981d 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -18,9 +18,20 @@ * Property hooks * * @see https://wiki.php.net/rfc/property-hooks + * @test lang.ast.unittest.emit.PropertyHooksTest */ trait PropertyHooks { + protected function rewriteHook($node, $virtual, $literal) { + if ($node instanceof Variable && 'field' === $node->pointer) return $virtual; + if ($node instanceof Literal && '__PROPERTY__' === $node->expression) return $literal; + + foreach ($node->children() as &$child) { + $child= $this->rewriteHook($child, $virtual, $literal); + } + return $node; + } + protected function emitProperty($result, $property) { static $lookup= [ 'public' => MODIFIER_PUBLIC, @@ -34,14 +45,12 @@ protected function emitProperty($result, $property) { if (empty($property->hooks)) return parent::emitProperty($result, $property); - $virtual= new InstanceExpression( - new Variable('this'), - new OffsetExpression(new Literal('__virtual'), new Literal("'{$property->name}'")) - ); + $scope= $result->codegen->scope[0]; + $literal= new Literal("'{$property->name}'"); + $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(new Literal('__virtual'), $literal)); - // Execute `set` hook, then assign virtual property to special variable $field if ($hook= $property->hooks['set'] ?? null) { - $set= new Block([$hook->expression, new Assignment($virtual, '=', new Variable('field'))]); + $set= new Block([$this->rewriteHook($hook->expression, $virtual, $literal)]); if ($hook->parameter) { if ('value' !== $hook->parameter->name) { @@ -64,20 +73,17 @@ protected function emitProperty($result, $property) { $set= new Assignment($virtual, '=', new Variable('value')); } - // Assign special variable $field to virtual property, then execute `get` hook if ($hook= $property->hooks['get'] ?? null) { - $get= new Block([ - new Assignment(new Variable('field'), '=', $virtual), - $hook->expression instanceof Block ? $hook->expression : new ReturnStatement($hook->expression) - ]); + $get= $this->rewriteHook( + $hook->expression instanceof Block ? $hook->expression : new ReturnStatement($hook->expression), + $virtual, + $literal + ); } else { $get= new ReturnStatement($virtual); } - $scope= $result->codegen->scope[0]; $scope->virtual[$property->name]= [$get, $set]; - - // Initialize via constructor if (isset($property->expression)) { $scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression; } diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index f246550c..65de215c 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -179,4 +179,17 @@ public function reflection() { Assert::equals('public string '.$t->getName().'::$test', $t->getField('test')->toString()); } + + #[Test] + public function property_constant() { + $r= $this->run('class { + public $test { get => __PROPERTY__; } + + public function run() { + return $this->test; + } + }'); + + Assert::equals('test', $r); + } } \ No newline at end of file From e3f2ae18f7f008c22ce0e62005dc2dcfa87b4192 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 14 May 2023 11:26:35 +0200 Subject: [PATCH 04/30] Do not declare virtual properties inside interfaces --- .../php/lang/ast/emit/PropertyHooks.class.php | 28 ++++++++++--------- .../unittest/emit/PropertyHooksTest.class.php | 13 +++++++-- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index 57b9981d..66c36f63 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -45,7 +45,22 @@ protected function emitProperty($result, $property) { if (empty($property->hooks)) return parent::emitProperty($result, $property); + // Emit XP meta information for the reflection API $scope= $result->codegen->scope[0]; + $modifiers= 0; + foreach ($property->modifiers as $name) { + $modifiers|= $lookup[$name]; + } + $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] + ]; + if ('interface' === $scope->type->kind) return; + + // Declare virtual properties with __set and __get $literal= new Literal("'{$property->name}'"); $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(new Literal('__virtual'), $literal)); @@ -87,18 +102,5 @@ protected function emitProperty($result, $property) { if (isset($property->expression)) { $scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression; } - - // Emit XP meta information for the reflection API - $modifiers= 0; - foreach ($property->modifiers as $name) { - $modifiers|= $lookup[$name]; - } - $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] - ]; } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index 65de215c..873319bb 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -35,8 +35,8 @@ public function run() { Assert::equals('Test', $r); } -# - #[Test] + + #[Test] public function abbreviated_get() { $r= $this->run('class { private $word= "Test"; @@ -192,4 +192,13 @@ public function run() { Assert::equals('test', $r); } + + #[Test] + public function reflection_of_interface_fields() { + $t= $this->type('interface { + public $test { get; } + }'); + + Assert::equals('public var '.$t->getName().'::$test', $t->getField('test')->toString()); + } } \ No newline at end of file From ffe6c4bfe7d928137be90f6d766b803103f988af Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 14 May 2023 21:17:55 +0200 Subject: [PATCH 05/30] Change implementation to emit `__[hook]_[name]()` methods This way, stack traces / warnings will point to the correct location, we have a way to invoke parent hooks, and interface and abstract hooks will be subject to implementation checks --- .../php/lang/ast/emit/PropertyHooks.class.php | 76 ++++++++++--------- .../unittest/emit/PropertyHooksTest.class.php | 23 ++++++ 2 files changed, 64 insertions(+), 35 deletions(-) diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index 66c36f63..83ee3305 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -3,12 +3,12 @@ use lang\ast\nodes\{ Assignment, Block, - Braced, - ClosureExpression, InstanceExpression, InvokeExpression, Literal, + Method, OffsetExpression, + Parameter, ReturnStatement, Signature, Variable @@ -58,47 +58,53 @@ protected function emitProperty($result, $property) { DETAIL_TARGET_ANNO => [], DETAIL_ARGUMENTS => [$modifiers] ]; - if ('interface' === $scope->type->kind) return; - // Declare virtual properties with __set and __get $literal= new Literal("'{$property->name}'"); $virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(new Literal('__virtual'), $literal)); - if ($hook= $property->hooks['set'] ?? null) { - $set= new Block([$this->rewriteHook($hook->expression, $virtual, $literal)]); - - if ($hook->parameter) { - if ('value' !== $hook->parameter->name) { - array_unshift($set->statements, new Assignment( - new Variable($hook->parameter->name), - '=', - new Variable('value') - )); - } - - // Perform type checking if parameter is typed using an IIFE - if ($hook->parameter->type) { - array_unshift($set->statements, new InvokeExpression( - new Braced(new ClosureExpression(new Signature([$hook->parameter], null), [], [])), - [new Variable('value')] - )); - } + // Emit get and set hooks in-place. Ignore any unknown hooks + $get= $set= null; + foreach ($property->hooks as $type => $hook) { + $method= '__'.$type.'_'.$property->name; + if ('get' === $type) { + $this->emitOne($result, new Method( + $hook->modifiers, + $method, + new Signature([], null), + null === $hook->expression ? null : [$this->rewriteHook( + $hook->expression instanceof Block ? $hook->expression : new ReturnStatement($hook->expression), + $virtual, + $literal + )], + $hook->annotations + )); + $get= new ReturnStatement(new InvokeExpression( + new InstanceExpression(new Variable('this'), new Literal($method)), + [] + )); + } else if ('set' === $type) { + $this->emitOne($result, new Method( + $hook->modifiers, + $method, + new Signature($hook->parameter ? [$hook->parameter] : [new Parameter('value', null)], null), + null === $hook->expression ? null : [$this->rewriteHook($hook->expression, $virtual, $literal)], + $hook->annotations + )); + $set= new InvokeExpression( + new InstanceExpression(new Variable('this'), new Literal($method)), + [new Variable('value')] + ); } - } else { - $set= new Assignment($virtual, '=', new Variable('value')); } - if ($hook= $property->hooks['get'] ?? null) { - $get= $this->rewriteHook( - $hook->expression instanceof Block ? $hook->expression : new ReturnStatement($hook->expression), - $virtual, - $literal - ); - } else { - $get= new ReturnStatement($virtual); - } + // Declare virtual properties with __set and __get as well as initializations + // except inside interfaces, which cannot contain properties. + if ('interface' === $scope->type->kind) return; - $scope->virtual[$property->name]= [$get, $set]; + $scope->virtual[$property->name]= [ + $get ?? new ReturnStatement($virtual), + $set ?? new Assignment($virtual, '=', new Variable('value')) + ]; if (isset($property->expression)) { $scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression; } diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index 873319bb..95934cfb 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -201,4 +201,27 @@ public function reflection_of_interface_fields() { Assert::equals('public var '.$t->getName().'::$test', $t->getField('test')->toString()); } + + #[Test] + public function line_number_in_thrown_expression() { + $r= $this->run('use lang\\IllegalArgumentException; class { + public $test { + set(string $name) { + if (strlen($name) > 10) throw new IllegalArgumentException("Too long"); + $field= $name; + } + } + + public function run() { + try { + $this->test= "this is too long"; + return null; + } catch (IllegalArgumentException $expected) { + return $expected->getLine(); + } + } + }'); + + Assert::equals(4, $r); + } } \ No newline at end of file From 4138f723e9bcfc52a004adb02eaef5f635aec905 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 14 May 2023 21:22:58 +0200 Subject: [PATCH 06/30] QA: Reorder code --- .../unittest/emit/PropertyHooksTest.class.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index 95934cfb..d2d96ff0 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -168,18 +168,6 @@ public function run() { Assert::equals('Test', $r); } - #[Test] - public function reflection() { - $t= $this->type('class { - public string $test { - get => $field; - set => $field= ucfirst($value); - } - }'); - - Assert::equals('public string '.$t->getName().'::$test', $t->getField('test')->toString()); - } - #[Test] public function property_constant() { $r= $this->run('class { @@ -193,6 +181,18 @@ public function run() { Assert::equals('test', $r); } + #[Test] + public function reflection() { + $t= $this->type('class { + public string $test { + get => $field; + set => $field= ucfirst($value); + } + }'); + + Assert::equals('public string '.$t->getName().'::$test', $t->getField('test')->toString()); + } + #[Test] public function reflection_of_interface_fields() { $t= $this->type('interface { From c9d348c0d328b591f1a9be812004330fdba3aa06 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 14 May 2023 21:24:29 +0200 Subject: [PATCH 07/30] Test abstract hooks --- .../ast/unittest/emit/PropertyHooksTest.class.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index d2d96ff0..1ab53cf4 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -194,7 +194,18 @@ public function reflection() { } #[Test] - public function reflection_of_interface_fields() { + public function abstract_hook() { + $t= $this->type('abstract class { + public string $test { + abstract get; + } + }'); + + Assert::equals('public string '.$t->getName().'::$test', $t->getField('test')->toString()); + } + + #[Test] + public function interface_hook() { $t= $this->type('interface { public $test { get; } }'); From c92001d4a357a3458bf94d802fccfd0eccf8f77c Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 14 May 2023 21:38:32 +0200 Subject: [PATCH 08/30] QA: Make tests consistent --- .../lang/ast/unittest/emit/PropertyHooksTest.class.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index 1ab53cf4..8be6ef19 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -196,9 +196,7 @@ public function reflection() { #[Test] public function abstract_hook() { $t= $this->type('abstract class { - public string $test { - abstract get; - } + public string $test { abstract get; } }'); Assert::equals('public string '.$t->getName().'::$test', $t->getField('test')->toString()); @@ -207,10 +205,10 @@ public function abstract_hook() { #[Test] public function interface_hook() { $t= $this->type('interface { - public $test { get; } + public string $test { get; } }'); - Assert::equals('public var '.$t->getName().'::$test', $t->getField('test')->toString()); + Assert::equals('public string '.$t->getName().'::$test', $t->getField('test')->toString()); } #[Test] From e74781946d2f9a002ab8ffc97ffdb6e8c5b1d4f2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 14 May 2023 21:54:09 +0200 Subject: [PATCH 09/30] Make $this->propertyName work The RFC states: "Using $this->propertyName directly is supported, but not recommended" --- .../php/lang/ast/emit/PropertyHooks.class.php | 22 +++++++++++++++---- .../unittest/emit/PropertyHooksTest.class.php | 17 ++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index 83ee3305..c3d076ee 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -22,12 +22,20 @@ */ trait PropertyHooks { - protected function rewriteHook($node, $virtual, $literal) { - if ($node instanceof Variable && 'field' === $node->pointer) return $virtual; + protected function rewriteHook($node, $name, $virtual, $literal) { + + // Magic constant referencing property nae if ($node instanceof Literal && '__PROPERTY__' === $node->expression) return $literal; + // Special variable $field, $this->propertyName syntax + if ($node instanceof Variable && 'field' === $node->pointer || ( + $node instanceof InstanceExpression && + $node->expression instanceof Variable && 'this' === $node->expression->pointer && + $node->member instanceof Literal && $name === $node->member->expression + )) return $virtual; + foreach ($node->children() as &$child) { - $child= $this->rewriteHook($child, $virtual, $literal); + $child= $this->rewriteHook($child, $name, $virtual, $literal); } return $node; } @@ -73,6 +81,7 @@ protected function emitProperty($result, $property) { new Signature([], null), null === $hook->expression ? null : [$this->rewriteHook( $hook->expression instanceof Block ? $hook->expression : new ReturnStatement($hook->expression), + $property->name, $virtual, $literal )], @@ -87,7 +96,12 @@ protected function emitProperty($result, $property) { $hook->modifiers, $method, new Signature($hook->parameter ? [$hook->parameter] : [new Parameter('value', null)], null), - null === $hook->expression ? null : [$this->rewriteHook($hook->expression, $virtual, $literal)], + null === $hook->expression ? null : [$this->rewriteHook( + $hook->expression, + $property->name, + $virtual, + $literal + )], $hook->annotations )); $set= new InvokeExpression( diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index 8be6ef19..6a8d656f 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -108,6 +108,23 @@ public function run() { Assert::equals('Test', $r); } + #[Test] + public function get_and_set_using_property() { + $r= $this->run('class { + public $test { + get => $this->test; + set => $this->test= ucfirst($value); + } + + public function run() { + $this->test= "test"; + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + #[Test] public function implicit_set() { $r= $this->run('class { From 3756cf41d54d0ece8f4994d93f40f34c7063ff8a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 15 May 2023 20:39:20 +0200 Subject: [PATCH 10/30] Support abstract properties --- src/main/php/lang/ast/emit/PropertyHooks.class.php | 5 +++-- .../lang/ast/unittest/emit/PropertyHooksTest.class.php | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index c3d076ee..c1602c64 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -74,9 +74,10 @@ protected function emitProperty($result, $property) { $get= $set= null; foreach ($property->hooks as $type => $hook) { $method= '__'.$type.'_'.$property->name; + $modifierList= $modifiers & MODIFIER_ABSTRACT ? ['abstract'] : $hook->modifiers; if ('get' === $type) { $this->emitOne($result, new Method( - $hook->modifiers, + $modifierList, $method, new Signature([], null), null === $hook->expression ? null : [$this->rewriteHook( @@ -93,7 +94,7 @@ protected function emitProperty($result, $property) { )); } else if ('set' === $type) { $this->emitOne($result, new Method( - $hook->modifiers, + $modifierList, $method, new Signature($hook->parameter ? [$hook->parameter] : [new Parameter('value', null)], null), null === $hook->expression ? null : [$this->rewriteHook( diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index 6a8d656f..cd9a8573 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -219,6 +219,15 @@ public function abstract_hook() { Assert::equals('public string '.$t->getName().'::$test', $t->getField('test')->toString()); } + #[Test] + public function abstract_property() { + $t= $this->type('abstract class { + public abstract string $test { get; set; } + }'); + + Assert::equals('public abstract string '.$t->getName().'::$test', $t->getField('test')->toString()); + } + #[Test] public function interface_hook() { $t= $this->type('interface { From 9830d042f9b1a537c44ab8fa96f78ec194895752 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 18 May 2023 15:30:15 +0200 Subject: [PATCH 11/30] Fix "Undefined property: lang\ast\nodes\Hook::$annotations" --- src/main/php/lang/ast/emit/PropertyHooks.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index c1602c64..d4288bbe 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -86,7 +86,7 @@ protected function emitProperty($result, $property) { $virtual, $literal )], - $hook->annotations + null // $hook->annotations )); $get= new ReturnStatement(new InvokeExpression( new InstanceExpression(new Variable('this'), new Literal($method)), @@ -103,7 +103,7 @@ protected function emitProperty($result, $property) { $virtual, $literal )], - $hook->annotations + null // $hook->annotations )); $set= new InvokeExpression( new InstanceExpression(new Variable('this'), new Literal($method)), From ed5003f44f6de3b3c3db7e152a1c40392b7fcfb6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 18 May 2023 15:46:26 +0200 Subject: [PATCH 12/30] Add scope checks to private and protected properties --- .../php/lang/ast/emit/PropertyHooks.class.php | 29 ++++++-- .../unittest/emit/PropertyHooksTest.class.php | 70 +++++++++++++++++++ 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index d4288bbe..5701b9e7 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -1,5 +1,6 @@ MODIFIER_PUBLIC, @@ -88,10 +109,10 @@ protected function emitProperty($result, $property) { )], null // $hook->annotations )); - $get= new ReturnStatement(new InvokeExpression( + $get= $this->withScopeCheck($modifiers, $property->name, new ReturnStatement(new InvokeExpression( new InstanceExpression(new Variable('this'), new Literal($method)), [] - )); + ))); } else if ('set' === $type) { $this->emitOne($result, new Method( $modifierList, @@ -105,10 +126,10 @@ protected function emitProperty($result, $property) { )], null // $hook->annotations )); - $set= new InvokeExpression( + $set= $this->withScopeCheck($modifiers, $property->name, new InvokeExpression( new InstanceExpression(new Variable('this'), new Literal($method)), [new Variable('value')] - ); + )); } } diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index cd9a8573..16d4ed31 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -259,4 +259,74 @@ public function run() { Assert::equals(4, $r); } + + #[Test] + public function accessing_private_property() { + $r= $this->run('class { + private string $test { get => "Test"; } + + public function run() { + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test] + public function accessing_protected_property() { + $r= $this->run('class { + protected string $test { get => "Test"; } + + public function run() { + return $this->test; + } + }'); + + Assert::equals('Test', $r); + } + + #[Test, Expect(class: Error::class, message: '/Cannot access private property .+test/')] + public function accessing_private_property_from_outside() { + $r= $this->run('class { + private string $test { get => "Test"; } + + public function run() { + return $this; + } + }'); + + $r->test; + } + + #[Test, Expect(class: Error::class, message: '/Cannot access protected property .+test/')] + public function accessing_protected_property_from_outside() { + $r= $this->run('class { + protected string $test { get => "Test"; } + + public function run() { + return $this; + } + }'); + + $r->test; + } + + #[Test] + public function accessing_private_property_reflectively() { + $t= $this->type('class { + private string $test { get => "Test"; } + }'); + + Assert::equals('Test', $t->getField('test')->setAccessible(true)->get($t->newInstance())); + } + + #[Test] + public function accessing_protected_property_reflectively() { + $t= $this->type('class { + protected string $test { get => "Test"; } + }'); + + Assert::equals('Test', $t->getField('test')->setAccessible(true)->get($t->newInstance())); + } } \ No newline at end of file From 6031c35e9d902d163079aad9399ca6dbbf53ac10 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 18 May 2023 15:47:58 +0200 Subject: [PATCH 13/30] Simplify withScopeCheck() --- src/main/php/lang/ast/emit/PropertyHooks.class.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index 5701b9e7..a0e02840 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -41,24 +41,24 @@ protected function rewriteHook($node, $name, $virtual, $literal) { return $node; } - protected function withScopeCheck($modifiers, $name, $node) { + protected function withScopeCheck($modifiers, $node) { 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__."::\\$%1$s");' + 'throw new \\Error("Cannot access private property ".__CLASS__."::".$name);' ); } 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__."::\\$%1$s");' + 'throw new \\Error("Cannot access protected property ".__CLASS__."::".$name);' ); } else { return $node; } - return new Block([new Code(sprintf($check, $name)), $node]); + return new Block([new Code($check), $node]); } protected function emitProperty($result, $property) { @@ -109,7 +109,7 @@ protected function emitProperty($result, $property) { )], null // $hook->annotations )); - $get= $this->withScopeCheck($modifiers, $property->name, new ReturnStatement(new InvokeExpression( + $get= $this->withScopeCheck($modifiers, new ReturnStatement(new InvokeExpression( new InstanceExpression(new Variable('this'), new Literal($method)), [] ))); @@ -126,7 +126,7 @@ protected function emitProperty($result, $property) { )], null // $hook->annotations )); - $set= $this->withScopeCheck($modifiers, $property->name, new InvokeExpression( + $set= $this->withScopeCheck($modifiers, new InvokeExpression( new InstanceExpression(new Variable('this'), new Literal($method)), [new Variable('value')] )); From f21a615a6143667b21006630943026b7c3e124e1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Thu, 18 May 2023 16:19:34 +0200 Subject: [PATCH 14/30] Add support for for `::$field::hook()` --- .../php/lang/ast/emit/PropertyHooks.class.php | 19 ++++++++++++++++++- .../unittest/emit/PropertyHooksTest.class.php | 16 ++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index a0e02840..391ba59c 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -11,6 +11,7 @@ OffsetExpression, Parameter, ReturnStatement, + ScopeExpression, Signature, Variable }; @@ -25,7 +26,7 @@ trait PropertyHooks { protected function rewriteHook($node, $name, $virtual, $literal) { - // Magic constant referencing property nae + // Magic constant referencing property name if ($node instanceof Literal && '__PROPERTY__' === $node->expression) return $literal; // Special variable $field, $this->propertyName syntax @@ -35,6 +36,22 @@ protected function rewriteHook($node, $name, $virtual, $literal) { $node->member instanceof Literal && $name === $node->member->expression )) return $virtual; + // ::$field::hook() => ::___() + if ( + $node instanceof ScopeExpression && + $node->member instanceof InvokeExpression && + $node->member->expression instanceof Literal && + $node->type instanceof ScopeExpression && + $node->type->member instanceof Variable && + is_string($node->type->type) && + is_string($node->type->member->pointer) + ) { + return new ScopeExpression($node->type->type, new InvokeExpression( + new Literal('__'.$node->member->expression->expression.'_'.$node->type->member->pointer), + $node->member->arguments + )); + } + foreach ($node->children() as &$child) { $child= $this->rewriteHook($child, $name, $virtual, $literal); } diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index 16d4ed31..a4dcaf77 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -329,4 +329,20 @@ public function accessing_protected_property_reflectively() { Assert::equals('Test', $t->getField('test')->setAccessible(true)->get($t->newInstance())); } + + #[Test] + public function get_parent_hook() { + $base= $this->type('class { + public string $test { get => "Test"; } + }'); + $r= $this->run('class extends '.$base->literal().' { + public string $test { get => parent::$test::get()."!"; } + + public function run() { + return $this->test; + } + }'); + + Assert::equals('Test!', $r); + } } \ No newline at end of file From c32235c2517e97af827cf2c15e441c8df9076358 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 May 2023 16:07:18 +0200 Subject: [PATCH 15/30] Implement get-hooks to use by-reference semantics --- src/main/php/lang/ast/emit/PHP.class.php | 2 +- .../php/lang/ast/emit/PropertyHooks.class.php | 25 +++++++++++-------- .../unittest/emit/PropertyHooksTest.class.php | 17 +++++++++++++ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 81d94301..7eb6bd17 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -441,7 +441,7 @@ protected function emitClass($result, $class) { } $result->out->write('];'); - $result->out->write('public function __get($name) { switch ($name) {'); + $result->out->write('public function &__get($name) { switch ($name) {'); foreach ($context->virtual as $name => $access) { $result->out->write($name ? 'case "'.$name.'":' : 'default:'); $this->emitOne($result, $access[0]); diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index 391ba59c..df25e423 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -58,7 +58,7 @@ protected function rewriteHook($node, $name, $virtual, $literal) { return $node; } - protected function withScopeCheck($modifiers, $node) { + protected function withScopeCheck($modifiers, $nodes) { if ($modifiers & MODIFIER_PRIVATE) { $check= ( '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'. @@ -71,11 +71,13 @@ protected function withScopeCheck($modifiers, $node) { 'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'. 'throw new \\Error("Cannot access protected property ".__CLASS__."::".$name);' ); + } else if (1 === sizeof($nodes)) { + return $nodes[0]; } else { - return $node; + return new Block($nodes); } - return new Block([new Code($check), $node]); + return new Block([new Code($check), ...$nodes]); } protected function emitProperty($result, $property) { @@ -117,7 +119,7 @@ protected function emitProperty($result, $property) { $this->emitOne($result, new Method( $modifierList, $method, - new Signature([], null), + new Signature([], null, $hook->byref), null === $hook->expression ? null : [$this->rewriteHook( $hook->expression instanceof Block ? $hook->expression : new ReturnStatement($hook->expression), $property->name, @@ -126,10 +128,13 @@ protected function emitProperty($result, $property) { )], null // $hook->annotations )); - $get= $this->withScopeCheck($modifiers, new ReturnStatement(new InvokeExpression( - new InstanceExpression(new Variable('this'), new Literal($method)), - [] - ))); + $get= $this->withScopeCheck($modifiers, [ + new Assignment(new Variable('r'), $hook->byref ? '=&' : '=', new InvokeExpression( + new InstanceExpression(new Variable('this'), new Literal($method)), + [] + )), + new ReturnStatement(new Variable('r')) + ]); } else if ('set' === $type) { $this->emitOne($result, new Method( $modifierList, @@ -143,10 +148,10 @@ protected function emitProperty($result, $property) { )], null // $hook->annotations )); - $set= $this->withScopeCheck($modifiers, new InvokeExpression( + $set= $this->withScopeCheck($modifiers, [new InvokeExpression( new InstanceExpression(new Variable('this'), new Literal($method)), [new Variable('value')] - )); + )]); } } diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index a4dcaf77..fe16a1e7 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -185,6 +185,23 @@ public function run() { Assert::equals('Test', $r); } + #[Test] + public function by_reference_supports_array_modifications() { + $r= $this->run('class { + private $list= []; + public $test { + &get => $this->list; + } + + public function run() { + $this->test[]= "Test"; + return $this->test; + } + }'); + + Assert::equals(['Test'], $r); + } + #[Test] public function property_constant() { $r= $this->run('class { From d96a4871ac82b35acb44e7f443c6d17415306f7a Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 May 2023 16:10:23 +0200 Subject: [PATCH 16/30] Fix "Only variable references should be returned by reference" --- src/main/php/lang/ast/emit/ReadonlyProperties.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php index 5dcbee40..3356b426 100755 --- a/src/main/php/lang/ast/emit/ReadonlyProperties.class.php +++ b/src/main/php/lang/ast/emit/ReadonlyProperties.class.php @@ -55,7 +55,7 @@ protected function emitProperty($result, $property) { // Create virtual property implementing the readonly semantics $scope->virtual[$property->name]= [ - new Code(sprintf($check.'return $this->__virtual["%1$s"][0] ?? null;', $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}");'. From c1f1e61097faec6bfdcb901f58efb4a11ec89e9f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 21 May 2023 16:11:24 +0200 Subject: [PATCH 17/30] Fix PHP 7.0 compatibility --- src/main/php/lang/ast/emit/PropertyHooks.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index df25e423..bd294520 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -77,7 +77,8 @@ protected function withScopeCheck($modifiers, $nodes) { return new Block($nodes); } - return new Block([new Code($check), ...$nodes]); + array_unshift($nodes, new Code($check)); + return new Block($nodes); } protected function emitProperty($result, $property) { From 5aa77806b0470d071788e05da82269041f55edc9 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 6 Jun 2023 21:12:33 +0200 Subject: [PATCH 18/30] Migrate to new reflection library --- .../unittest/emit/PropertyHooksTest.class.php | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index fe16a1e7..bcb2f4e8 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -12,7 +12,7 @@ class PropertyHooksTest extends EmittingTest { #[Test] public function get_expression() { - $r= $this->run('class { + $r= $this->run('class %T { public $test { get => "Test"; } public function run() { @@ -25,7 +25,7 @@ public function run() { #[Test] public function get_block() { - $r= $this->run('class { + $r= $this->run('class %T { public $test { get { return "Test"; } } public function run() { @@ -38,7 +38,7 @@ public function run() { #[Test] public function abbreviated_get() { - $r= $this->run('class { + $r= $this->run('class %T { private $word= "Test"; private $interpunction= "!"; @@ -54,7 +54,7 @@ public function run() { #[Test] public function set_expression() { - $r= $this->run('class { + $r= $this->run('class %T { public $test { set => $field= ucfirst($value); } public function run() { @@ -68,7 +68,7 @@ public function run() { #[Test] public function set_block() { - $r= $this->run('class { + $r= $this->run('class %T { public $test { set($value) { $field= ucfirst($value); } } public function run() { @@ -82,7 +82,7 @@ public function run() { #[Test, Expect(IllegalArgumentException::class)] public function set_raising_exception() { - $this->run('use lang\\IllegalArgumentException; class { + $this->run('use lang\\IllegalArgumentException; class %T { public $test { set($value) { throw new IllegalArgumentException("Cannot set"); } } public function run() { @@ -93,7 +93,7 @@ public function run() { #[Test] public function get_and_set_using_field() { - $r= $this->run('class { + $r= $this->run('class %T { public $test { get => $field; set => $field= ucfirst($value); @@ -110,7 +110,7 @@ public function run() { #[Test] public function get_and_set_using_property() { - $r= $this->run('class { + $r= $this->run('class %T { public $test { get => $this->test; set => $this->test= ucfirst($value); @@ -127,7 +127,7 @@ public function run() { #[Test] public function implicit_set() { - $r= $this->run('class { + $r= $this->run('class %T { public $test { get => ucfirst($field); } @@ -143,7 +143,7 @@ public function run() { #[Test] public function typed_set() { - $r= $this->run('use util\\Bytes; class { + $r= $this->run('use util\\Bytes; class %T { public string $test { set(string|Bytes $arg) => $field= ucfirst($arg); } @@ -159,7 +159,7 @@ public function run() { #[Test, Expect(class: Error::class, message: '/Argument .+ type int(eger)?, string given/')] public function typed_mismatch() { - $this->run('class { + $this->run('class %T { public string $test { set(int $times) => $field= $times." times"; } @@ -172,7 +172,7 @@ public function run() { #[Test] public function initial_value() { - $r= $this->run('class { + $r= $this->run('class %T { public $test= "test" { get => ucfirst($field); } @@ -187,7 +187,7 @@ public function run() { #[Test] public function by_reference_supports_array_modifications() { - $r= $this->run('class { + $r= $this->run('class %T { private $list= []; public $test { &get => $this->list; @@ -204,7 +204,7 @@ public function run() { #[Test] public function property_constant() { - $r= $this->run('class { + $r= $this->run('class %T { public $test { get => __PROPERTY__; } public function run() { @@ -217,46 +217,46 @@ public function run() { #[Test] public function reflection() { - $t= $this->type('class { + $t= $this->declare('class %T { public string $test { get => $field; set => $field= ucfirst($value); } }'); - Assert::equals('public string '.$t->getName().'::$test', $t->getField('test')->toString()); + Assert::equals('public string $test', $t->property('test')->toString()); } #[Test] public function abstract_hook() { - $t= $this->type('abstract class { + $t= $this->declare('abstract class %T { public string $test { abstract get; } }'); - Assert::equals('public string '.$t->getName().'::$test', $t->getField('test')->toString()); + Assert::equals('public string $test', $t->property('test')->toString()); } #[Test] public function abstract_property() { - $t= $this->type('abstract class { + $t= $this->declare('abstract class %T { public abstract string $test { get; set; } }'); - Assert::equals('public abstract string '.$t->getName().'::$test', $t->getField('test')->toString()); + Assert::equals('public abstract string $test', $t->property('test')->toString()); } #[Test] public function interface_hook() { - $t= $this->type('interface { + $t= $this->declare('interface %T { public string $test { get; } }'); - Assert::equals('public string '.$t->getName().'::$test', $t->getField('test')->toString()); + Assert::equals('public string $test', $t->property('test')->toString()); } #[Test] public function line_number_in_thrown_expression() { - $r= $this->run('use lang\\IllegalArgumentException; class { + $r= $this->run('use lang\\IllegalArgumentException; class %T { public $test { set(string $name) { if (strlen($name) > 10) throw new IllegalArgumentException("Too long"); @@ -279,7 +279,7 @@ public function run() { #[Test] public function accessing_private_property() { - $r= $this->run('class { + $r= $this->run('class %T { private string $test { get => "Test"; } public function run() { @@ -292,7 +292,7 @@ public function run() { #[Test] public function accessing_protected_property() { - $r= $this->run('class { + $r= $this->run('class %T { protected string $test { get => "Test"; } public function run() { @@ -305,7 +305,7 @@ public function run() { #[Test, Expect(class: Error::class, message: '/Cannot access private property .+test/')] public function accessing_private_property_from_outside() { - $r= $this->run('class { + $r= $this->run('class %T { private string $test { get => "Test"; } public function run() { @@ -318,7 +318,7 @@ public function run() { #[Test, Expect(class: Error::class, message: '/Cannot access protected property .+test/')] public function accessing_protected_property_from_outside() { - $r= $this->run('class { + $r= $this->run('class %T { protected string $test { get => "Test"; } public function run() { @@ -331,28 +331,28 @@ public function run() { #[Test] public function accessing_private_property_reflectively() { - $t= $this->type('class { + $t= $this->declare('class %T { private string $test { get => "Test"; } }'); - Assert::equals('Test', $t->getField('test')->setAccessible(true)->get($t->newInstance())); + Assert::equals('Test', $t->property('test')->get($t->newInstance(), $t)); } #[Test] public function accessing_protected_property_reflectively() { - $t= $this->type('class { + $t= $this->declare('class %T { protected string $test { get => "Test"; } }'); - Assert::equals('Test', $t->getField('test')->setAccessible(true)->get($t->newInstance())); + Assert::equals('Test', $t->property('test')->get($t->newInstance(), $t)); } #[Test] public function get_parent_hook() { - $base= $this->type('class { + $base= $this->declare('class %T { public string $test { get => "Test"; } }'); - $r= $this->run('class extends '.$base->literal().' { + $r= $this->run('class %T extends '.$base->literal().' { public string $test { get => parent::$test::get()."!"; } public function run() { From 8584d20b52e67b91dcfba71eed498a5b155318b8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 24 Feb 2024 12:36:42 +0100 Subject: [PATCH 19/30] Remove support for `$field` See https://externals.io/message/122445#122478: > However, since it seems no one likes $field, we have removed it from the RFC --- .../php/lang/ast/emit/PropertyHooks.class.php | 6 ++-- .../unittest/emit/PropertyHooksTest.class.php | 35 +++++-------------- 2 files changed, 12 insertions(+), 29 deletions(-) diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index bd294520..6ab32d17 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -29,12 +29,12 @@ protected function rewriteHook($node, $name, $virtual, $literal) { // Magic constant referencing property name if ($node instanceof Literal && '__PROPERTY__' === $node->expression) return $literal; - // Special variable $field, $this->propertyName syntax - if ($node instanceof Variable && 'field' === $node->pointer || ( + // Rewrite $this->propertyName to virtual property + if ( $node instanceof InstanceExpression && $node->expression instanceof Variable && 'this' === $node->expression->pointer && $node->member instanceof Literal && $name === $node->member->expression - )) return $virtual; + ) return $virtual; // ::$field::hook() => ::___() if ( diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index bcb2f4e8..0a1b7542 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -55,7 +55,7 @@ public function run() { #[Test] public function set_expression() { $r= $this->run('class %T { - public $test { set => $field= ucfirst($value); } + public $test { set => $this->test= ucfirst($value); } public function run() { $this->test= "test"; @@ -69,7 +69,7 @@ public function run() { #[Test] public function set_block() { $r= $this->run('class %T { - public $test { set($value) { $field= ucfirst($value); } } + public $test { set($value) { $this->test= ucfirst($value); } } public function run() { $this->test= "test"; @@ -91,23 +91,6 @@ public function run() { }'); } - #[Test] - public function get_and_set_using_field() { - $r= $this->run('class %T { - public $test { - get => $field; - set => $field= ucfirst($value); - } - - public function run() { - $this->test= "test"; - return $this->test; - } - }'); - - Assert::equals('Test', $r); - } - #[Test] public function get_and_set_using_property() { $r= $this->run('class %T { @@ -129,7 +112,7 @@ public function run() { public function implicit_set() { $r= $this->run('class %T { public $test { - get => ucfirst($field); + get => ucfirst($this->test); } public function run() { @@ -145,7 +128,7 @@ public function run() { public function typed_set() { $r= $this->run('use util\\Bytes; class %T { public string $test { - set(string|Bytes $arg) => $field= ucfirst($arg); + set(string|Bytes $arg) => $this->test= ucfirst($arg); } public function run() { @@ -161,7 +144,7 @@ public function run() { public function typed_mismatch() { $this->run('class %T { public string $test { - set(int $times) => $field= $times." times"; + set(int $times) => $this->test= $times." times"; } public function run() { @@ -174,7 +157,7 @@ public function run() { public function initial_value() { $r= $this->run('class %T { public $test= "test" { - get => ucfirst($field); + get => ucfirst($this->test); } public function run() { @@ -219,8 +202,8 @@ public function run() { public function reflection() { $t= $this->declare('class %T { public string $test { - get => $field; - set => $field= ucfirst($value); + get => $this->test; + set => $this->test= ucfirst($value); } }'); @@ -260,7 +243,7 @@ public function line_number_in_thrown_expression() { public $test { set(string $name) { if (strlen($name) > 10) throw new IllegalArgumentException("Too long"); - $field= $name; + $this->test= $name; } } From 5929c83e8c6de220de129c1e852e152f001bf443 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 16 Mar 2024 17:57:13 +0100 Subject: [PATCH 20/30] Implement short set in accordance with RFC See https://wiki.php.net/rfc/property-hooks#short-set, changed from original --- src/main/php/lang/ast/emit/PropertyHooks.class.php | 2 +- .../lang/ast/unittest/emit/PropertyHooksTest.class.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index 6ab32d17..747722f1 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -142,7 +142,7 @@ protected function emitProperty($result, $property) { $method, new Signature($hook->parameter ? [$hook->parameter] : [new Parameter('value', null)], null), null === $hook->expression ? null : [$this->rewriteHook( - $hook->expression, + $hook->expression instanceof Block ? $hook->expression : new Assignment($virtual, '=', $hook->expression), $property->name, $virtual, $literal diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index 0a1b7542..366091d1 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -55,7 +55,7 @@ public function run() { #[Test] public function set_expression() { $r= $this->run('class %T { - public $test { set => $this->test= ucfirst($value); } + public $test { set => ucfirst($value); } public function run() { $this->test= "test"; @@ -96,7 +96,7 @@ public function get_and_set_using_property() { $r= $this->run('class %T { public $test { get => $this->test; - set => $this->test= ucfirst($value); + set => ucfirst($value); } public function run() { @@ -128,7 +128,7 @@ public function run() { public function typed_set() { $r= $this->run('use util\\Bytes; class %T { public string $test { - set(string|Bytes $arg) => $this->test= ucfirst($arg); + set(string|Bytes $arg) => ucfirst($arg); } public function run() { @@ -144,7 +144,7 @@ public function run() { public function typed_mismatch() { $this->run('class %T { public string $test { - set(int $times) => $this->test= $times." times"; + set(int $times) => $times." times"; } public function run() { @@ -203,7 +203,7 @@ public function reflection() { $t= $this->declare('class %T { public string $test { get => $this->test; - set => $this->test= ucfirst($value); + set => ucfirst($value); } }'); From 2fbca6f9a05e08ef98f0874767a71df460e0550b Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 16 Mar 2024 19:09:01 +0100 Subject: [PATCH 21/30] Use simplemost `set { ... }` form --- src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index 366091d1..0c34de5e 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -69,7 +69,7 @@ public function run() { #[Test] public function set_block() { $r= $this->run('class %T { - public $test { set($value) { $this->test= ucfirst($value); } } + public $test { set { $this->test= ucfirst($value); } } public function run() { $this->test= "test"; From 0b823d55f900393c1283a6573cd3b7c74afc244e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 16 Mar 2024 19:10:04 +0100 Subject: [PATCH 22/30] Use simplemost `set { ... }` form for block with exceptions --- src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index 0c34de5e..c44e26f2 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -83,7 +83,7 @@ public function run() { #[Test, Expect(IllegalArgumentException::class)] public function set_raising_exception() { $this->run('use lang\\IllegalArgumentException; class %T { - public $test { set($value) { throw new IllegalArgumentException("Cannot set"); } } + public $test { set { throw new IllegalArgumentException("Cannot set"); } } public function run() { $this->test= "test"; From 39f49298b4d3d72175364301f422324ae7d2a70f Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 18 May 2024 19:03:42 +0800 Subject: [PATCH 23/30] Remove abstract hooks, they are no longer included in the RFC See https://wiki.php.net/rfc/property-hooks#abstract_properties --- .../lang/ast/unittest/emit/PropertyHooksTest.class.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index c44e26f2..0b74e45b 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -210,15 +210,6 @@ public function reflection() { Assert::equals('public string $test', $t->property('test')->toString()); } - #[Test] - public function abstract_hook() { - $t= $this->declare('abstract class %T { - public string $test { abstract get; } - }'); - - Assert::equals('public string $test', $t->property('test')->toString()); - } - #[Test] public function abstract_property() { $t= $this->declare('abstract class %T { From df4a8891492de049aafbd806ef4ccb7419702a09 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 18 May 2024 19:08:14 +0800 Subject: [PATCH 24/30] Fix "Type of parameter $times of hook T::$test::set must be compatible with property type" --- .../lang/ast/unittest/emit/PropertyHooksTest.class.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index 0b74e45b..1b21aff0 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -140,15 +140,15 @@ public function run() { Assert::equals('Test', $r); } - #[Test, Expect(class: Error::class, message: '/Argument .+ type int(eger)?, string given/')] + #[Test, Expect(class: Error::class, message: '/Argument .+ type string, array given/')] public function typed_mismatch() { $this->run('class %T { public string $test { - set(int $times) => $times." times"; + set(string $times) => $times." times"; } public function run() { - $this->test= "no"; + $this->test= []; } }'); } @@ -225,14 +225,14 @@ public function interface_hook() { public string $test { get; } }'); - Assert::equals('public string $test', $t->property('test')->toString()); + Assert::equals('public abstract string $test', $t->property('test')->toString()); } #[Test] public function line_number_in_thrown_expression() { $r= $this->run('use lang\\IllegalArgumentException; class %T { public $test { - set(string $name) { + set($name) { if (strlen($name) > 10) throw new IllegalArgumentException("Too long"); $this->test= $name; } From 9a4186a63c8abd8260d1a04a817c35b3891dffba Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 18 May 2024 19:09:08 +0800 Subject: [PATCH 25/30] Emit property hooks natively if its PR has been merged --- .../php/lang/ast/emit/PropertyHooks.class.php | 67 +++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index 747722f1..b9daff2f 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -1,5 +1,6 @@ MODIFIER_PUBLIC, 'protected' => MODIFIER_PROTECTED, @@ -92,20 +93,19 @@ protected function emitProperty($result, $property) { 'readonly' => 0x0080, // XP 10.13: MODIFIER_READONLY ]; - if (empty($property->hooks)) return parent::emitProperty($result, $property); - // Emit XP meta information for the reflection API $scope= $result->codegen->scope[0]; $modifiers= 0; foreach ($property->modifiers as $name) { $modifiers|= $lookup[$name]; } + $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] + DETAIL_ARGUMENTS => ['interface' === $scope->type->kind ? $modifiers | MODIFIER_ABSTRACT : $modifiers] ]; $literal= new Literal("'{$property->name}'"); @@ -168,4 +168,63 @@ protected function emitProperty($result, $property) { $scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression; } } + + protected function emitNativeHooks($result, $property) { + $result->codegen->scope[0]->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 => [] + ]; + + $property->comment && $this->emitOne($result, $property->comment); + $property->annotations && $this->emitOne($result, $property->annotations); + $result->at($property->declared)->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name); + 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->codegen->scope[0]->statics['self::$'.$property->name]= $property->expression; + } else { + $result->codegen->scope[0]->init['$this->'.$property->name]= $property->expression; + } + } + + // TODO move this to lang.ast.emit.PHP once https://github.com/php/php-src/pull/13455 is merged + $result->out->write('{'); + foreach ($property->hooks as $type => $hook) { + $hook->byref && $result->out->write('&'); + $result->out->write($type); + if ($hook->parameter) { + $result->out->write('('); + $this->emitOne($result, $hook->parameter); + $result->out->write(')'); + } + + if (null === $hook->expression) { + $result->out->write(';'); + } else if ($hook->expression instanceof Block) { + $this->emitOne($result, $hook->expression); + } else { + $result->out->write('=>'); + $this->emitOne($result, $hook->expression); + $result->out->write(';'); + } + } + $result->out->write('}'); + } + + protected function emitProperty($result, $property) { + static $hooks= null; + + if (empty($property->hooks)) { + parent::emitProperty($result, $property); + } else if ($hooks ?? $hooks= method_exists(ReflectionProperty::class, 'getHooks')) { + $this->emitNativeHooks($result, $property); + } else { + $this->emitEmulatedHooks($result, $property); + } + } } \ No newline at end of file From 53f0256396d70046b096cb33fdc705fee50569e8 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 18 May 2024 19:09:45 +0800 Subject: [PATCH 26/30] Change emitScope() to no longer include type in braces except for `new` --- src/main/php/lang/ast/emit/PHP.class.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/php/lang/ast/emit/PHP.class.php b/src/main/php/lang/ast/emit/PHP.class.php index 44061644..d83c5cc7 100755 --- a/src/main/php/lang/ast/emit/PHP.class.php +++ b/src/main/php/lang/ast/emit/PHP.class.php @@ -11,6 +11,7 @@ Expression, InstanceExpression, Literal, + NewExpression, Property, ScopeExpression, UnpackExpression, @@ -1084,15 +1085,14 @@ protected function emitInvoke($result, $invoke) { protected function emitScope($result, $scope) { - // $x:: vs. e.g. invoke():: vs. T:: - if ($scope->type instanceof Variable) { + // new T():: vs. e.g. $x:: vs. T:: + if ($scope->type instanceof NewExpression) { + $result->out->write('('); $this->emitOne($result, $scope->type); - $result->out->write('::'); + $result->out->write(')::'); } else if ($scope->type instanceof Node) { - $t= $result->temp(); - $result->out->write('(null==='.$t.'='); $this->emitOne($result, $scope->type); - $result->out->write(")?null:{$t}::"); + $result->out->write('::'); } else { $result->out->write("{$scope->type}::"); } @@ -1111,7 +1111,7 @@ protected function emitScope($result, $scope) { } protected function emitInstance($result, $instance) { - if ('new' === $instance->expression->kind) { + if ($instance->expression instanceof NewExpression) { $result->out->write('('); $this->emitOne($result, $instance->expression); $result->out->write(')->'); From 7cd4039f04dc88e6ae5263631406109f6f2ac488 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 18 May 2024 19:44:05 +0800 Subject: [PATCH 27/30] Refactor chaining scope operators for PHP 7.x --- .../ast/emit/ChainScopeOperators.class.php | 32 +++++++++++++++++++ src/main/php/lang/ast/emit/PHP74.class.php | 2 +- .../ast/emit/RewriteClassOnObjects.class.php | 23 ------------- ....php => ChainScopeOperatorsTest.class.php} | 6 ++-- 4 files changed, 36 insertions(+), 27 deletions(-) create mode 100755 src/main/php/lang/ast/emit/ChainScopeOperators.class.php delete mode 100755 src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php rename src/test/php/lang/ast/unittest/emit/{RewriteClassOnObjectsTest.class.php => ChainScopeOperatorsTest.class.php} (84%) diff --git a/src/main/php/lang/ast/emit/ChainScopeOperators.class.php b/src/main/php/lang/ast/emit/ChainScopeOperators.class.php new file mode 100755 index 00000000..13bd4411 --- /dev/null +++ b/src/main/php/lang/ast/emit/ChainScopeOperators.class.php @@ -0,0 +1,32 @@ +type instanceof Node)) return $this->rewriteDynamicClassConstants($result, $scope); + + if ($scope->member instanceof Literal && 'class' === $scope->member->expression) { + $result->out->write('\\get_class('); + $this->emitOne($result, $scope->type); + $result->out->write(')'); + } else { + $t= $result->temp(); + $result->out->write('(null==='.$t.'='); + $this->emitOne($result, $scope->type); + $result->out->write(")?null:{$t}::"); + $this->emitOne($result, $scope->member); + } + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/emit/PHP74.class.php b/src/main/php/lang/ast/emit/PHP74.class.php index aaba1323..72a60b59 100755 --- a/src/main/php/lang/ast/emit/PHP74.class.php +++ b/src/main/php/lang/ast/emit/PHP74.class.php @@ -13,6 +13,7 @@ class PHP74 extends PHP { ArrayUnpackUsingMerge, AttributesAsComments, CallablesAsClosures, + ChainScopeOperators, MatchAsTernaries, NonCapturingCatchVariables, NullsafeAsTernaries, @@ -20,7 +21,6 @@ class PHP74 extends PHP { OmitConstantTypes, ReadonlyClasses, RewriteBlockLambdaExpressions, - RewriteClassOnObjects, RewriteEnums, RewriteExplicitOctals, RewriteProperties, diff --git a/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php b/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php deleted file mode 100755 index c1da8601..00000000 --- a/src/main/php/lang/ast/emit/RewriteClassOnObjects.class.php +++ /dev/null @@ -1,23 +0,0 @@ -member instanceof Literal && 'class' === $scope->member->expression && !is_string($scope->type)) { - $result->out->write('\\get_class('); - $this->emitOne($result, $scope->type); - $result->out->write(')'); - } else { - $this->rewriteDynamicClassConstants($result, $scope); - } - } -} \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/emit/RewriteClassOnObjectsTest.class.php b/src/test/php/lang/ast/unittest/emit/ChainScopeOperatorsTest.class.php similarity index 84% rename from src/test/php/lang/ast/unittest/emit/RewriteClassOnObjectsTest.class.php rename to src/test/php/lang/ast/unittest/emit/ChainScopeOperatorsTest.class.php index 147bde28..7e874e67 100755 --- a/src/test/php/lang/ast/unittest/emit/RewriteClassOnObjectsTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/ChainScopeOperatorsTest.class.php @@ -1,16 +1,16 @@ Date: Sat, 18 May 2024 21:02:00 +0800 Subject: [PATCH 28/30] Add tests for reflective access to virtual properties --- .../unittest/emit/PropertyHooksTest.class.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php index 1b21aff0..9c302b6d 100755 --- a/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php +++ b/src/test/php/lang/ast/unittest/emit/PropertyHooksTest.class.php @@ -219,6 +219,27 @@ public function abstract_property() { Assert::equals('public abstract string $test', $t->property('test')->toString()); } + #[Test] + public function reflective_get() { + $t= $this->declare('class %T { + public string $test { get => "Test"; } + }'); + + $instance= $t->newInstance(); + Assert::equals('Test', $t->property('test')->get($instance)); + } + + #[Test] + public function reflective_set() { + $t= $this->declare('class %T { + public string $test { set => ucfirst($value); } + }'); + + $instance= $t->newInstance(); + $t->property('test')->set($instance, 'test'); + Assert::equals('Test', $instance->test); + } + #[Test] public function interface_hook() { $t= $this->declare('interface %T { From e0b89f326c1b877c7026cab76c1427278f5d87ab Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jun 2024 10:35:33 +0200 Subject: [PATCH 29/30] Simplify code See https://github.com/xp-framework/compiler/pull/166/files/53f0256396d70046b096cb33fdc705fee50569e8#r1199775185 --- src/main/php/lang/ast/emit/PropertyHooks.class.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/php/lang/ast/emit/PropertyHooks.class.php b/src/main/php/lang/ast/emit/PropertyHooks.class.php index b9daff2f..621edbfb 100755 --- a/src/main/php/lang/ast/emit/PropertyHooks.class.php +++ b/src/main/php/lang/ast/emit/PropertyHooks.class.php @@ -78,8 +78,7 @@ protected function withScopeCheck($modifiers, $nodes) { return new Block($nodes); } - array_unshift($nodes, new Code($check)); - return new Block($nodes); + return new Block([new Code($check), ...$nodes]); } protected function emitEmulatedHooks($result, $property) { From e605a976ed6228968522f84e3d7eb0a5ebddbdf6 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sat, 15 Jun 2024 10:46:45 +0200 Subject: [PATCH 30/30] Use release version of AST library --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 107b536b..ed40d3f1 100755 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "require" : { "xp-framework/core": "^12.0 | ^11.6 | ^10.16", "xp-framework/reflection": "^3.0 | ^2.13", - "xp-framework/ast": "dev-feature/property-hooks as 11.1.0", + "xp-framework/ast": "^11.1", "php" : ">=7.4.0" }, "require-dev" : {