Skip to content

Commit 07b351a

Browse files
committed
MFH
2 parents 00d9658 + 4b3514e commit 07b351a

35 files changed

+1253
-103
lines changed

ChangeLog.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,52 @@ XP Compiler ChangeLog
33

44
## ?.?.? / ????-??-??
55

6+
## 9.3.2 / 2024-11-02
7+
8+
* Fixed empty match expressions and match expressions with default
9+
case only in PHP 7
10+
(@thekid)
11+
* Added tests verifying closures are supported in constant expressions
12+
https://wiki.php.net/rfc/closures_in_const_expr
13+
(@thekid)
14+
15+
## 9.3.1 / 2024-10-05
16+
17+
* Fixed `private(set)` not being implicitely marked as *final*, see
18+
https://wiki.php.net/rfc/asymmetric-visibility-v2#inheritance
19+
(@thekid)
20+
* Fixed enclosing scopes when using references - @thekid
21+
22+
## 9.3.0 / 2024-08-31
23+
24+
* Fixed checks for property hooks emulation with asymmetric visibility
25+
(@thekid)
26+
* Added PHP 8.4 emitter which natively emits property hooks and asymmetric
27+
visibility syntax. This is integration-tested with PHP 8.4.0 Beta 4.
28+
See https://github.com/php/php-src/blob/php-8.4.0beta4/NEWS
29+
(@thekid)
30+
* Changed emitter to use native readonly classes in PHP 8.2, fixing an
31+
inconsistency with accessing undefined properties
32+
(@thekid)
33+
34+
## 9.2.0 / 2024-08-27
35+
36+
* Merged PR #183: Add emitting support for asymmetric visibility. See
37+
https://wiki.php.net/rfc/asymmetric-visibility-v2, targeted for PHP 8.4
38+
(@thekid)
39+
40+
## 9.1.1 / 2024-08-27
41+
42+
* Forward compatibility with newer `xp-framework/ast` releases - @thekid
43+
44+
## 9.1.0 / 2024-06-15
45+
46+
* Merged PR #166: Implement property hooks via virtual properties, see
47+
https://wiki.php.net/rfc/property-hooks. Includes support for native
48+
implementation, which is yet to be merged (php/php-src#13455). Thus,
49+
this still might be a moving target in some regards!
50+
(@thekid)
51+
652
## 9.0.0 / 2024-03-23
753

854
* Merged PR #179: XP 12 compatibility, dropping PHP 7.0 - 7.3 support!

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ XP Compiler
66
[![BSD Licence](https://raw.githubusercontent.com/xp-framework/web/master/static/licence-bsd.png)](https://github.com/xp-framework/core/blob/master/LICENCE.md)
77
[![Requires PHP 7.4+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_4plus.svg)](http://php.net/)
88
[![Supports PHP 8.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-8_0plus.svg)](http://php.net/)
9-
[![Latest Stable Version](https://poser.pugx.org/xp-framework/compiler/version.png)](https://packagist.org/packages/xp-framework/compiler)
9+
[![Latest Stable Version](https://poser.pugx.org/xp-framework/compiler/version.svg)](https://packagist.org/packages/xp-framework/compiler)
1010

1111
Compiles future PHP to today's PHP.
1212

@@ -16,24 +16,25 @@ After adding the compiler to your project via `composer require xp-framework/com
1616

1717
Example
1818
-------
19-
The following code uses Hack language, PHP 8.3, PHP 8.2, 8.1 and 8.0 features but runs on anything >= PHP 7.4. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary.
19+
The following code uses Hack language, PHP 8.4, PHP 8.3, PHP 8.2, 8.1 and 8.0 features but runs on anything >= PHP 7.4. Builtin features from newer PHP versions are translated to work with the currently executing runtime if necessary.
2020

2121
```php
2222
<?php // In a file "HelloWorld.php"
2323

2424
use lang\Reflection;
25+
use util\Date;
2526
use util\cmd\Console;
2627

2728
#[Author('Timm Friebe')]
2829
#[Permissions(0o777)]
2930
class HelloWorld {
30-
public const string GREETING = 'Hello';
31+
private const string GREETING= 'Hello';
3132

3233
public static function main(array<string> $args): void {
3334
$greet= fn($to, $from) => self::GREETING.' '.$to.' from '.$from;
3435
$author= Reflection::type(self::class)->annotation(Author::class)->argument(0);
3536

36-
Console::writeLine($greet($args[0] ?? 'World', from: $author));
37+
Console::writeLine(new Date()->toString(), ': ', $greet($args[0] ?? 'World', from: $author));
3738
}
3839
}
3940
```
@@ -90,8 +91,9 @@ Usage: xp compile <in> [<out>]
9091
lang.ast.emit.PHP74
9192
lang.ast.emit.PHP80
9293
lang.ast.emit.PHP81
93-
lang.ast.emit.PHP82 [*]
94-
lang.ast.emit.PHP83
94+
lang.ast.emit.PHP82
95+
lang.ast.emit.PHP83 [*]
96+
lang.ast.emit.PHP84
9597
lang.ast.syntax.php.Using [*]
9698

9799
@FileSystemCL<./vendor/xp-lang/php-is-operator/src/main/php>

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
"keywords": ["module", "xp"],
88
"require" : {
99
"xp-framework/core": "^12.0 | ^11.6 | ^10.16",
10-
"xp-framework/reflection": "^3.0 | ^2.13",
11-
"xp-framework/ast": "dev-feature/pipelines as 11.1.0",
10+
"xp-framework/reflection": "^3.2 | ^2.15",
11+
"xp-framework/ast": "dev-feature/pipelines as 11.4.0",
1212
"php" : ">=7.4.0"
1313
},
1414
"require-dev" : {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php namespace lang\ast\emit;
2+
3+
use lang\ast\nodes\{
4+
Assignment,
5+
Block,
6+
InstanceExpression,
7+
Literal,
8+
OffsetExpression,
9+
ReturnStatement,
10+
Variable
11+
};
12+
13+
/**
14+
* Asymmetric Visibility
15+
*
16+
* @see https://wiki.php.net/rfc/asymmetric-visibility-v2
17+
* @test lang.ast.unittest.emit.AsymmetricVisibilityTest
18+
*/
19+
trait AsymmetricVisibility {
20+
use VisibilityChecks;
21+
22+
protected function emitProperty($result, $property) {
23+
static $lookup= [
24+
'public' => MODIFIER_PUBLIC,
25+
'protected' => MODIFIER_PROTECTED,
26+
'private' => MODIFIER_PRIVATE,
27+
'static' => MODIFIER_STATIC,
28+
'final' => MODIFIER_FINAL,
29+
'abstract' => MODIFIER_ABSTRACT,
30+
'readonly' => MODIFIER_READONLY,
31+
'public(set)' => 0x1000000,
32+
'protected(set)' => 0x0000800,
33+
'private(set)' => 0x0001000,
34+
];
35+
36+
$scope= $result->codegen->scope[0];
37+
$modifiers= 0;
38+
foreach ($property->modifiers as $name) {
39+
$modifiers|= $lookup[$name];
40+
}
41+
42+
// Declare checks for private(set) and protected(set), folding declarations
43+
// like `[visibility] [visibility](set)` to just the visibility itself.
44+
if ($modifiers & 0x1000000) {
45+
$checks= [];
46+
$modifiers&= ~0x1000000;
47+
} else if ($modifiers & 0x0000800) {
48+
$checks= [$this->protected($property->name, 'modify protected(set)')];
49+
$modifiers & MODIFIER_PROTECTED && $modifiers&= ~0x0000800;
50+
} else if ($modifiers & 0x0001000) {
51+
$checks= [$this->private($property->name, 'modify private(set)')];
52+
$modifiers & MODIFIER_PRIVATE ? $modifiers&= ~0x0001000 : $modifiers|= MODIFIER_FINAL;
53+
}
54+
55+
// Emit XP meta information for the reflection API
56+
$scope->meta[self::PROPERTY][$property->name]= [
57+
DETAIL_RETURNS => $property->type ? $property->type->name() : 'var',
58+
DETAIL_ANNOTATIONS => $property->annotations,
59+
DETAIL_COMMENT => $property->comment,
60+
DETAIL_TARGET_ANNO => [],
61+
DETAIL_ARGUMENTS => [$modifiers]
62+
];
63+
64+
// The readonly flag is really two flags in one: write-once and restricted(set)
65+
if (in_array('readonly', $property->modifiers)) {
66+
$checks[]= $this->initonce($property->name);
67+
}
68+
69+
$virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(
70+
new Literal('__virtual'),
71+
new Literal("'{$property->name}'"))
72+
);
73+
$scope->virtual[$property->name]= [
74+
new ReturnStatement($virtual),
75+
new Block([...$checks, new Assignment($virtual, '=', new Variable('value'))]),
76+
];
77+
if (isset($property->expression)) {
78+
$scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression;
79+
}
80+
}
81+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php namespace lang\ast\emit;
2+
3+
use lang\ast\Node;
4+
use lang\ast\nodes\Literal;
5+
6+
/**
7+
* Allows chaining of scope operators and rewrites `[expr]::class` to `get_class($object)`
8+
* except if expression references a type - e.g. `self` or `ClassName`.
9+
*
10+
* @see https://wiki.php.net/rfc/class_name_literal_on_object
11+
* @see https://wiki.php.net/rfc/variable_syntax_tweaks#constant_dereferencability
12+
* @test lang.ast.unittest.emit.ChainScopeOperatorsTest
13+
*/
14+
trait ChainScopeOperators {
15+
use RewriteDynamicClassConstants { emitScope as rewriteDynamicClassConstants; }
16+
17+
protected function emitScope($result, $scope) {
18+
if (!($scope->type instanceof Node)) return $this->rewriteDynamicClassConstants($result, $scope);
19+
20+
if ($scope->member instanceof Literal && 'class' === $scope->member->expression) {
21+
$result->out->write('\\get_class(');
22+
$this->emitOne($result, $scope->type);
23+
$result->out->write(')');
24+
} else {
25+
$t= $result->temp();
26+
$result->out->write('(null==='.$t.'=');
27+
$this->emitOne($result, $scope->type);
28+
$result->out->write(")?null:{$t}::");
29+
$this->emitOne($result, $scope->member);
30+
}
31+
}
32+
}

src/main/php/lang/ast/emit/MatchAsTernaries.class.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ protected function emitMatch($result, $match) {
3030
}
3131
}
3232

33+
// Match without cases => create something that will never match
34+
$b || $result->out->write('===NAN?:');
35+
3336
// Emit IIFE for raising an error until we have throw expressions
3437
if (null === $match->default) {
35-
$result->out->write('function() use('.$t.') { throw new \\Error("Unhandled match value of type ".gettype('.$t.')); })(');
38+
$result->out->write('(function() use('.$t.') { throw new \\Error("Unhandled match value of type ".gettype('.$t.')); })()');
3639
} else {
3740
$this->emitAsExpression($result, $match->default);
3841
}

src/main/php/lang/ast/emit/PHP.class.php

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
Expression,
1414
InstanceExpression,
1515
Literal,
16+
NewExpression,
1617
Property,
1718
ScopeExpression,
1819
UnpackExpression,
@@ -126,7 +127,7 @@ protected function enclose($result, $node, $signature, $static, $emit) {
126127
$capture= [];
127128
foreach ($result->codegen->search($node, 'variable') as $var) {
128129
if (isset($result->locals[$var->pointer])) {
129-
$capture[$var->pointer]= true;
130+
$capture[$var->pointer]??= ($result->locals[$var->pointer] ? '&$' : '$').$var->pointer;
130131
}
131132
}
132133
unset($capture['this']);
@@ -144,9 +145,9 @@ protected function enclose($result, $node, $signature, $static, $emit) {
144145
}
145146

146147
if ($capture) {
147-
$result->out->write('use($'.implode(', $', array_keys($capture)).')');
148-
foreach ($capture as $name => $_) {
149-
$result->locals[$name]= true;
148+
$result->out->write('use('.implode(', ', $capture).')');
149+
foreach ($capture as $name => $variable) {
150+
$result->locals[$name]= '&' === $variable[0];
150151
}
151152
}
152153

@@ -299,7 +300,7 @@ protected function emitArray($result, $array) {
299300
}
300301

301302
protected function emitParameter($result, $parameter) {
302-
$result->locals[$parameter->name]= true;
303+
$result->locals[$parameter->name]= $parameter->reference;
303304
$parameter->annotations && $this->emitOne($result, $parameter->annotations);
304305

305306
// If we have a non-constant default and a type, emit a nullable type hint
@@ -341,7 +342,7 @@ protected function emitSignature($result, $signature, $use= null) {
341342
if ($use) {
342343
$result->out->write(' use('.implode(',', $use).') ');
343344
foreach ($use as $variable) {
344-
$result->locals[substr($variable, 1)]= true;
345+
$result->locals[ltrim($variable, '&$')]= '&' === $variable[0];
345346
}
346347
}
347348

@@ -464,7 +465,7 @@ protected function emitClass($result, $class) {
464465
}
465466
$result->out->write('];');
466467

467-
$result->out->write('public function __get($name) { switch ($name) {');
468+
$result->out->write('public function &__get($name) { switch ($name) {');
468469
foreach ($context->virtual as $name => $access) {
469470
$result->out->write($name ? 'case "'.$name.'":' : 'default:');
470471
$this->emitOne($result, $access[0]);
@@ -646,12 +647,37 @@ protected function emitProperty($result, $property) {
646647
$result->codegen->scope[0]->init['$this->'.$property->name]= $property->expression;
647648
}
648649
}
649-
$result->out->write(';');
650+
651+
if ($property->hooks) {
652+
$result->out->write('{');
653+
foreach ($property->hooks as $type => $hook) {
654+
$hook->byref && $result->out->write('&');
655+
$result->out->write($type);
656+
if ($hook->parameter) {
657+
$result->out->write('(');
658+
$this->emitOne($result, $hook->parameter);
659+
$result->out->write(')');
660+
}
661+
662+
if (null === $hook->expression) {
663+
$result->out->write(';');
664+
} else if ($hook->expression instanceof Block) {
665+
$this->emitOne($result, $hook->expression);
666+
} else {
667+
$result->out->write('=>');
668+
$this->emitOne($result, $hook->expression);
669+
$result->out->write(';');
670+
}
671+
}
672+
$result->out->write('}');
673+
} else {
674+
$result->out->write(';');
675+
}
650676
}
651677

652678
protected function emitMethod($result, $method) {
653679
$locals= $result->locals;
654-
$result->locals= ['this' => true];
680+
$result->locals= ['this' => false];
655681
$meta= [
656682
DETAIL_RETURNS => $method->signature->returns ? $method->signature->returns->name() : 'var',
657683
DETAIL_ANNOTATIONS => $method->annotations,
@@ -719,7 +745,11 @@ protected function emitMethod($result, $method) {
719745
}
720746

721747
foreach ($promoted as $param) {
722-
$this->emitProperty($result, new Property(explode(' ', $param->promote), $param->name, $param->type));
748+
$this->emitProperty($result, new Property(
749+
is_array($param->promote) ? $param->promote : explode(' ', $param->promote),
750+
$param->name,
751+
$param->type
752+
));
723753
}
724754

725755
$result->locals= $locals;
@@ -770,7 +800,7 @@ protected function emitOffset($result, $offset) {
770800
protected function emitAssign($result, $target) {
771801
if ($target instanceof Variable && $target->const) {
772802
$result->out->write('$'.$target->pointer);
773-
$result->locals[$target->pointer]= true;
803+
$result->locals[$target->pointer]= false;
774804
} else if ($target instanceof ArrayLiteral) {
775805
$result->out->write('[');
776806
foreach ($target->values as $pair) {
@@ -1086,15 +1116,14 @@ protected function emitInvoke($result, $invoke) {
10861116

10871117
protected function emitScope($result, $scope) {
10881118

1089-
// $x::<expr> vs. e.g. invoke()::<expr> vs. T::<expr>
1090-
if ($scope->type instanceof Variable) {
1119+
// new T()::<expr> vs. e.g. $x::<expr> vs. T::<expr>
1120+
if ($scope->type instanceof NewExpression) {
1121+
$result->out->write('(');
10911122
$this->emitOne($result, $scope->type);
1092-
$result->out->write('::');
1123+
$result->out->write(')::');
10931124
} else if ($scope->type instanceof Node) {
1094-
$t= $result->temp();
1095-
$result->out->write('(null==='.$t.'=');
10961125
$this->emitOne($result, $scope->type);
1097-
$result->out->write(")?null:{$t}::");
1126+
$result->out->write('::');
10981127
} else {
10991128
$result->out->write("{$scope->type}::");
11001129
}
@@ -1113,7 +1142,7 @@ protected function emitScope($result, $scope) {
11131142
}
11141143

11151144
protected function emitInstance($result, $instance) {
1116-
if ('new' === $instance->expression->kind) {
1145+
if ($instance->expression instanceof NewExpression) {
11171146
$result->out->write('(');
11181147
$this->emitOne($result, $instance->expression);
11191148
$result->out->write(')->');

0 commit comments

Comments
 (0)